Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
da2e5bd
Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot
tastybento May 30, 2026
ad43941
Add files via upload
tastybento Jun 28, 2026
9b41211
ci: add CurseForge publish workflow (publishes the release asset)
tastybento Jun 28, 2026
be4b87a
ci: pin reusable workflow to SHA and enable Hangar publish
tastybento Jun 28, 2026
729f9e7
ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d…
tastybento Jun 28, 2026
96ba6b9
Merge pull request #38 from BentoBoxWorld/ci/curseforge-publish
tastybento Jun 28, 2026
0052c3d
ci: bump pinned reusable workflow to fe4b1f0
tastybento Jul 1, 2026
22c6ad0
Merge pull request #39 from BentoBoxWorld/ci/bump-publish-workflow-fe…
tastybento Jul 1, 2026
6efd980
ci: bump pinned publish-platforms.yml to ca2dcd1
tastybento Jul 3, 2026
693e6d6
Merge pull request #40 from BentoBoxWorld/ci/bump-publish-platforms-c…
tastybento Jul 3, 2026
a3be0bd
Reload re-imports control panels from the template file
tastybento Jul 12, 2026
794cf49
Merge pull request #41 from BentoBoxWorld/fix/reload-reimports-template
tastybento Jul 17, 2026
8f4777e
ci: bump publish-platforms pin to the multipart-JSON fix
tastybento Jul 29, 2026
8deb0fb
Merge pull request #42 from BentoBoxWorld/ci/publish-platforms-pin
tastybento Jul 30, 2026
f2cffa2
Update Chinese translations in zh-TW.yml
qwe664 Aug 4, 2026
bd56c9c
Merge pull request #43 from qwe664/develop
tastybento Aug 4, 2026
ef9c192
Lower minimum BentoBox version to 2.7.1 and bump to 1.16.1
tastybento Aug 31, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
# ControlPanel — .github/workflows/publish.yml
# Publishes the jar attached to a GitHub release to CurseForge via the
# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of
# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar
# githubactions:S7637). workflow_dispatch lets you (re)publish a given version.

name: Publish release to CurseForge

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.2.3)"
required: true
type: string

jobs:
publish:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "" # blank = skip Hangar
curseforge_id: "1514827"
game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5"
version: ${{ inputs.version }} # empty on release events -> falls back to the release tag
secrets:
HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
8 changes: 4 additions & 4 deletions pom.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>1.16.0</build.version>
<build.version>1.16.1</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_ControlPanel</sonar.projectKey>
Expand DownExpand Up@@ -140,9 +140,9 @@
<!-- Your addon must contain Paper and BentoBox APIs dependencies. -->
<dependencies>
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<version>v1.21-SNAPSHOT</version>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.110.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/world/bentobox/controlpanel/ControlPanelAddon.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,19 @@ public void onReload()
}
else
{
// Reload the database into cache first so wipe/re-import operate on a
// consistent view of what is currently stored.
this.manager.reload();

// Re-import each active game mode's panels from controlPanelTemplate.yml so
// that admin edits to the template take effect on reload. Without this the
// panels stay pinned to whatever was first seeded into the database.
this.getPlugin().getAddonsManager().getGameModeAddons().forEach(gameModeAddon -> {
if (!this.settings.getDisabledGameModes().contains(gameModeAddon.getDescription().getName()))
{
this.manager.reimportControlPanels(gameModeAddon);
}
});
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,9 +127,10 @@ public void setup()
@Override
public boolean execute(User user, String label, List<String> args)
{
ControlPanelManager manager = this.<ControlPanelAddon>getAddon().getAddonManager();
ControlPanelAddon addon = this.getAddon();
ControlPanelManager manager = addon.getAddonManager();

String fileName = args.size() == 1 ? args.get(0) : "controlPanelTemplate.yml";
String fileName = args.size() == 1 ? args.get(0) : addon.getSettings().getTemplateFile();

if (manager.hasAnyControlPanel(this.getWorld()))
{
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/world/bentobox/controlpanel/config/Settings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,28 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
}


/**
* This method returns the templateFile value.
*
* @return the value of templateFile.
*/
public String getTemplateFile()
{
return templateFile;
}


/**
* This method sets the templateFile value.
*
* @param templateFile the templateFile new value.
*/
public void setTemplateFile(String templateFile)
{
this.templateFile = templateFile;
}


// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
Expand All@@ -64,4 +86,12 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
@ConfigComment(" - BSkyBlock")
@ConfigEntry(path = "disabled-gamemodes")
private Set<String> disabledGameModes = new HashSet<>();

@ConfigComment("")
@ConfigComment("The template file that control panels are imported from.")
@ConfigComment("This file is read when panels are first seeded and re-read on every reload,")
@ConfigComment("so editing it and running 'bbox reload' will apply your changes.")
@ConfigComment("It must exist in the addon folder (addons/ControlPanel).")
@ConfigEntry(path = "template-file")
private String templateFile = "controlPanelTemplate.yml";
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,8 @@ public ControlPanelManager(ControlPanelAddon addon)
{
this.addon = addon;

// save template file into directory.
// Save the default template file into the directory. The configured template file
// may point elsewhere, but the packaged default is always named controlPanelTemplate.yml.
if (!new File(this.addon.getDataFolder(), "controlPanelTemplate.yml").exists())
{
this.addon.saveResource("controlPanelTemplate.yml", false);
Expand DownExpand Up@@ -128,6 +129,26 @@ public void reload()
}


/**
* This method re-imports the control panels for the given game mode from the
* template file configured via {@code template-file} in config.yml (defaults to
* {@code controlPanelTemplate.yml}), replacing whatever is currently stored in the
* database and cache.
* <p>
* This is what makes edits to the template file take effect on {@code bbox reload}:
* without it, {@link #reload()} only re-reads the database and template changes are
* silently ignored.
*
* @param gameModeAddon Game mode whose panels must be re-imported from the template.
*/
public void reimportControlPanels(@NotNull GameModeAddon gameModeAddon)
{
// Clear existing data so removed/renamed panels do not linger, then re-read the template.
this.wipeData(gameModeAddon, null);
this.importControlPanels(null, gameModeAddon);
}


// ---------------------------------------------------------------------
// Section: Save methods
// ---------------------------------------------------------------------
Expand DownExpand Up@@ -226,7 +247,7 @@ public void importControlPanels(@Nullable User user, @NotNull World world, @NotN
*/
public void importControlPanels(@Nullable User user, @NotNull GameModeAddon addon)
{
this.importControlPanels(user, addon, "controlPanelTemplate");
this.importControlPanels(user, addon, this.addon.getSettings().getTemplateFile());
}


Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/addon.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ repository: 'BentoBoxWorld/ControlPanel'
# Must use Material.values() with uppercase.
icon: 'COMMAND_BLOCK'
# Minimum BentoBox API version
api-version: 3.10.0
api-version: 2.7.1

# List of addon authors.
authors:
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,3 +5,9 @@
# disabled-gamemodes:
# - BSkyBlock
disabled-gamemodes: []
#
# The template file that control panels are imported from.
# This file is read when panels are first seeded and re-read on every reload,
# so editing it and running 'bbox reload' will apply your changes.
# It must exist in the addon folder (addons/ControlPanel).
template-file: controlPanelTemplate.yml
28 changes: 13 additions & 15 deletions src/main/resources/locales/zh-TW.yml
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
---
controlpanel:
messages:
import-count: 導入 [number] 控制面板
control-panels-wiped: "&2 所有為 [gamemode] 模式的控制面板已移除"
import-count: 已匯入 [number] 個控制面板。
control-panels-wiped: '&2已移除 [gamemode] 的所有控制面板。'
errors:
no-load: "&4 無法讀取 [file] 原因: [message]"
no-file: "&4 遺失 [file] 檔案"
no-valid-panels: "&4 未發現任何可用的控制面板"
no-valid-panels-op: "&4 未發現任何可用的控制面板. 你應該試著去導入他 請使用 /[admin] cp import"
not-a-gamemode-world: "&4 未發現任何有效的模式"
no-load: '&4無法載入檔案 [file][message]'
no-file: '&4找不到檔案 [file]。'
not-a-gamemode-world: '&4找不到有效的遊戲模式。'
no-valid-panels: '&4找不到任何有效的控制面板。'
no-valid-panels-op: '&4找不到任何有效的控制面板。請嘗試使用 /[admin] cp import 匯入。'
commands:
admin:
help:
description: '顯示管理員幫助指令

'
description: 顯示管理員說明指令。
import:
parameters: "[file-name] - 要導入的檔案的名子. 檔案為空 所以我們將創建 一般面板."
description: 導入控制面板從 [file]
confirmation: 請確認您要從 [gamemode] 中刪除現有控制面板,並從 [file] 中導入控制面板
parameters: '[file-name] - 要匯入的檔案名稱。留空則匯入預設範本。'
description: [file] 匯入控制面板。
confirmation: 請確認是否要移除 [gamemode] 目前的控制面板,並從 [file] 匯入新的控制面板。
help:
description: 開啟控制面板
parameters: ''
description: 開啟控制面板。
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,4 +35,15 @@ void testSetDisabledGameModes() {
assertTrue(settings.getDisabledGameModes().contains("BSkyBlock"));
assertTrue(settings.getDisabledGameModes().contains("AcidIsland"));
}

@Test
void testDefaultTemplateFile() {
assertEquals("controlPanelTemplate.yml", settings.getTemplateFile());
}

@Test
void testSetTemplateFile() {
settings.setTemplateFile("myPanels.yml");
assertEquals("myPanels.yml", settings.getTemplateFile());
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ public void setUp() throws Exception {

when(addon.getPlugin()).thenReturn(plugin);
when(addon.getLogger()).thenReturn(Logger.getAnonymousLogger());
when(addon.getSettings()).thenReturn(new world.bentobox.controlpanel.config.Settings());

File dataFolder = new File("addons/ControlPanel");
dataFolder.mkdirs();
Expand DownExpand Up@@ -260,6 +261,96 @@ void testReload() {
manager.reload();
}

@Test
void testReimportControlPanelsReadsTemplateFile() throws Exception {
// Make import's trailing getAddonManager().save() call resolve to this manager.
when(addon.getAddonManager()).thenReturn(manager);

// Write a template file that reimport should read from disk.
writeTemplate("""
panel-list:
default:
defaultPanel: true
panelName: 'From Template'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

// Template panel is now in the cache.
assertTrue(manager.hasAnyControlPanel(world));
ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Template", panel.getPanelName());
}

@Test
void testReimportControlPanelsRemovesDeletedPanels() throws Exception {
when(addon.getAddonManager()).thenReturn(manager);

// Seed the cache with an existing panel, as if previously stored in the database.
when(handler.loadObjects()).thenReturn(Collections.singletonList(createDefaultPanel()));
manager.reload();
assertTrue(manager.hasAnyControlPanel(world));

// A template with no panel-list entries should wipe the stored panel on reimport.
writeTemplate("panel-list: {}\n");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

assertFalse(manager.hasAnyControlPanel(world));
}

@Test
void testReimportControlPanelsUsesConfiguredTemplateFile() throws Exception {
// Point the config at a custom template file name.
world.bentobox.controlpanel.config.Settings settings =
new world.bentobox.controlpanel.config.Settings();
settings.setTemplateFile("customPanels.yml");
when(addon.getSettings()).thenReturn(settings);
when(addon.getAddonManager()).thenReturn(manager);

// The default template exists but is empty; only the custom file has a panel.
writeTemplate("panel-list: {}\n");
writeNamedTemplate("customPanels.yml", """
panel-list:
default:
defaultPanel: true
panelName: 'From Custom File'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Custom File", panel.getPanelName());
}

private void writeTemplate(String content) throws Exception {
writeNamedTemplate("controlPanelTemplate.yml", content);
}

private void writeNamedTemplate(String name, String content) throws Exception {
File templateFile = new File(new File("addons/ControlPanel"), name);
java.nio.file.Files.writeString(templateFile.toPath(), content);
}

private ControlPanelObject createDefaultPanel() {
ControlPanelObject cpo = new ControlPanelObject();
cpo.setUniqueId("BSkyBlock_default");
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
da2e5bd
Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot
tastybento May 30, 2026
ad43941
Add files via upload
tastybento Jun 28, 2026
9b41211
ci: add CurseForge publish workflow (publishes the release asset)
tastybento Jun 28, 2026
be4b87a
ci: pin reusable workflow to SHA and enable Hangar publish
tastybento Jun 28, 2026
729f9e7
ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d…
tastybento Jun 28, 2026
96ba6b9
Merge pull request #38 from BentoBoxWorld/ci/curseforge-publish
tastybento Jun 28, 2026
0052c3d
ci: bump pinned reusable workflow to fe4b1f0
tastybento Jul 1, 2026
22c6ad0
Merge pull request #39 from BentoBoxWorld/ci/bump-publish-workflow-fe…
tastybento Jul 1, 2026
6efd980
ci: bump pinned publish-platforms.yml to ca2dcd1
tastybento Jul 3, 2026
693e6d6
Merge pull request #40 from BentoBoxWorld/ci/bump-publish-platforms-c…
tastybento Jul 3, 2026
a3be0bd
Reload re-imports control panels from the template file
tastybento Jul 12, 2026
794cf49
Merge pull request #41 from BentoBoxWorld/fix/reload-reimports-template
tastybento Jul 17, 2026
8f4777e
ci: bump publish-platforms pin to the multipart-JSON fix
tastybento Jul 29, 2026
8deb0fb
Merge pull request #42 from BentoBoxWorld/ci/publish-platforms-pin
tastybento Jul 30, 2026
f2cffa2
Update Chinese translations in zh-TW.yml
qwe664 Aug 4, 2026
bd56c9c
Merge pull request #43 from qwe664/develop
tastybento Aug 4, 2026
ef9c192
Lower minimum BentoBox version to 2.7.1 and bump to 1.16.1
tastybento Aug 31, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
# ControlPanel — .github/workflows/publish.yml
# Publishes the jar attached to a GitHub release to CurseForge via the
# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of
# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar
# githubactions:S7637). workflow_dispatch lets you (re)publish a given version.

name: Publish release to CurseForge

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.2.3)"
required: true
type: string

jobs:
publish:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "" # blank = skip Hangar
curseforge_id: "1514827"
game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5"
version: ${{ inputs.version }} # empty on release events -> falls back to the release tag
secrets:
HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
8 changes: 4 additions & 4 deletions pom.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>1.16.0</build.version>
<build.version>1.16.1</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_ControlPanel</sonar.projectKey>
Expand DownExpand Up@@ -140,9 +140,9 @@
<!-- Your addon must contain Paper and BentoBox APIs dependencies. -->
<dependencies>
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<version>v1.21-SNAPSHOT</version>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.110.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/world/bentobox/controlpanel/ControlPanelAddon.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,19 @@ public void onReload()
}
else
{
// Reload the database into cache first so wipe/re-import operate on a
// consistent view of what is currently stored.
this.manager.reload();

// Re-import each active game mode's panels from controlPanelTemplate.yml so
// that admin edits to the template take effect on reload. Without this the
// panels stay pinned to whatever was first seeded into the database.
this.getPlugin().getAddonsManager().getGameModeAddons().forEach(gameModeAddon -> {
if (!this.settings.getDisabledGameModes().contains(gameModeAddon.getDescription().getName()))
{
this.manager.reimportControlPanels(gameModeAddon);
}
});
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,9 +127,10 @@ public void setup()
@Override
public boolean execute(User user, String label, List<String> args)
{
ControlPanelManager manager = this.<ControlPanelAddon>getAddon().getAddonManager();
ControlPanelAddon addon = this.getAddon();
ControlPanelManager manager = addon.getAddonManager();

String fileName = args.size() == 1 ? args.get(0) : "controlPanelTemplate.yml";
String fileName = args.size() == 1 ? args.get(0) : addon.getSettings().getTemplateFile();

if (manager.hasAnyControlPanel(this.getWorld()))
{
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/world/bentobox/controlpanel/config/Settings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,28 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
}


/**
* This method returns the templateFile value.
*
* @return the value of templateFile.
*/
public String getTemplateFile()
{
return templateFile;
}


/**
* This method sets the templateFile value.
*
* @param templateFile the templateFile new value.
*/
public void setTemplateFile(String templateFile)
{
this.templateFile = templateFile;
}


// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
Expand All@@ -64,4 +86,12 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
@ConfigComment(" - BSkyBlock")
@ConfigEntry(path = "disabled-gamemodes")
private Set<String> disabledGameModes = new HashSet<>();

@ConfigComment("")
@ConfigComment("The template file that control panels are imported from.")
@ConfigComment("This file is read when panels are first seeded and re-read on every reload,")
@ConfigComment("so editing it and running 'bbox reload' will apply your changes.")
@ConfigComment("It must exist in the addon folder (addons/ControlPanel).")
@ConfigEntry(path = "template-file")
private String templateFile = "controlPanelTemplate.yml";
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,8 @@ public ControlPanelManager(ControlPanelAddon addon)
{
this.addon = addon;

// save template file into directory.
// Save the default template file into the directory. The configured template file
// may point elsewhere, but the packaged default is always named controlPanelTemplate.yml.
if (!new File(this.addon.getDataFolder(), "controlPanelTemplate.yml").exists())
{
this.addon.saveResource("controlPanelTemplate.yml", false);
Expand DownExpand Up@@ -128,6 +129,26 @@ public void reload()
}


/**
* This method re-imports the control panels for the given game mode from the
* template file configured via {@code template-file} in config.yml (defaults to
* {@code controlPanelTemplate.yml}), replacing whatever is currently stored in the
* database and cache.
* <p>
* This is what makes edits to the template file take effect on {@code bbox reload}:
* without it, {@link #reload()} only re-reads the database and template changes are
* silently ignored.
*
* @param gameModeAddon Game mode whose panels must be re-imported from the template.
*/
public void reimportControlPanels(@NotNull GameModeAddon gameModeAddon)
{
// Clear existing data so removed/renamed panels do not linger, then re-read the template.
this.wipeData(gameModeAddon, null);
this.importControlPanels(null, gameModeAddon);
}


// ---------------------------------------------------------------------
// Section: Save methods
// ---------------------------------------------------------------------
Expand DownExpand Up@@ -226,7 +247,7 @@ public void importControlPanels(@Nullable User user, @NotNull World world, @NotN
*/
public void importControlPanels(@Nullable User user, @NotNull GameModeAddon addon)
{
this.importControlPanels(user, addon, "controlPanelTemplate");
this.importControlPanels(user, addon, this.addon.getSettings().getTemplateFile());
}


Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/addon.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ repository: 'BentoBoxWorld/ControlPanel'
# Must use Material.values() with uppercase.
icon: 'COMMAND_BLOCK'
# Minimum BentoBox API version
api-version: 3.10.0
api-version: 2.7.1

# List of addon authors.
authors:
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,3 +5,9 @@
# disabled-gamemodes:
# - BSkyBlock
disabled-gamemodes: []
#
# The template file that control panels are imported from.
# This file is read when panels are first seeded and re-read on every reload,
# so editing it and running 'bbox reload' will apply your changes.
# It must exist in the addon folder (addons/ControlPanel).
template-file: controlPanelTemplate.yml
28 changes: 13 additions & 15 deletions src/main/resources/locales/zh-TW.yml
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
---
controlpanel:
messages:
import-count: 導入 [number] 控制面板
control-panels-wiped: "&2 所有為 [gamemode] 模式的控制面板已移除"
import-count: 已匯入 [number] 個控制面板。
control-panels-wiped: '&2已移除 [gamemode] 的所有控制面板。'
errors:
no-load: "&4 無法讀取 [file] 原因: [message]"
no-file: "&4 遺失 [file] 檔案"
no-valid-panels: "&4 未發現任何可用的控制面板"
no-valid-panels-op: "&4 未發現任何可用的控制面板. 你應該試著去導入他 請使用 /[admin] cp import"
not-a-gamemode-world: "&4 未發現任何有效的模式"
no-load: '&4無法載入檔案 [file][message]'
no-file: '&4找不到檔案 [file]。'
not-a-gamemode-world: '&4找不到有效的遊戲模式。'
no-valid-panels: '&4找不到任何有效的控制面板。'
no-valid-panels-op: '&4找不到任何有效的控制面板。請嘗試使用 /[admin] cp import 匯入。'
commands:
admin:
help:
description: '顯示管理員幫助指令

'
description: 顯示管理員說明指令。
import:
parameters: "[file-name] - 要導入的檔案的名子. 檔案為空 所以我們將創建 一般面板."
description: 導入控制面板從 [file]
confirmation: 請確認您要從 [gamemode] 中刪除現有控制面板,並從 [file] 中導入控制面板
parameters: '[file-name] - 要匯入的檔案名稱。留空則匯入預設範本。'
description: [file] 匯入控制面板。
confirmation: 請確認是否要移除 [gamemode] 目前的控制面板,並從 [file] 匯入新的控制面板。
help:
description: 開啟控制面板
parameters: ''
description: 開啟控制面板。
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,4 +35,15 @@ void testSetDisabledGameModes() {
assertTrue(settings.getDisabledGameModes().contains("BSkyBlock"));
assertTrue(settings.getDisabledGameModes().contains("AcidIsland"));
}

@Test
void testDefaultTemplateFile() {
assertEquals("controlPanelTemplate.yml", settings.getTemplateFile());
}

@Test
void testSetTemplateFile() {
settings.setTemplateFile("myPanels.yml");
assertEquals("myPanels.yml", settings.getTemplateFile());
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ public void setUp() throws Exception {

when(addon.getPlugin()).thenReturn(plugin);
when(addon.getLogger()).thenReturn(Logger.getAnonymousLogger());
when(addon.getSettings()).thenReturn(new world.bentobox.controlpanel.config.Settings());

File dataFolder = new File("addons/ControlPanel");
dataFolder.mkdirs();
Expand DownExpand Up@@ -260,6 +261,96 @@ void testReload() {
manager.reload();
}

@Test
void testReimportControlPanelsReadsTemplateFile() throws Exception {
// Make import's trailing getAddonManager().save() call resolve to this manager.
when(addon.getAddonManager()).thenReturn(manager);

// Write a template file that reimport should read from disk.
writeTemplate("""
panel-list:
default:
defaultPanel: true
panelName: 'From Template'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

// Template panel is now in the cache.
assertTrue(manager.hasAnyControlPanel(world));
ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Template", panel.getPanelName());
}

@Test
void testReimportControlPanelsRemovesDeletedPanels() throws Exception {
when(addon.getAddonManager()).thenReturn(manager);

// Seed the cache with an existing panel, as if previously stored in the database.
when(handler.loadObjects()).thenReturn(Collections.singletonList(createDefaultPanel()));
manager.reload();
assertTrue(manager.hasAnyControlPanel(world));

// A template with no panel-list entries should wipe the stored panel on reimport.
writeTemplate("panel-list: {}\n");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

assertFalse(manager.hasAnyControlPanel(world));
}

@Test
void testReimportControlPanelsUsesConfiguredTemplateFile() throws Exception {
// Point the config at a custom template file name.
world.bentobox.controlpanel.config.Settings settings =
new world.bentobox.controlpanel.config.Settings();
settings.setTemplateFile("customPanels.yml");
when(addon.getSettings()).thenReturn(settings);
when(addon.getAddonManager()).thenReturn(manager);

// The default template exists but is empty; only the custom file has a panel.
writeTemplate("panel-list: {}\n");
writeNamedTemplate("customPanels.yml", """
panel-list:
default:
defaultPanel: true
panelName: 'From Custom File'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Custom File", panel.getPanelName());
}

private void writeTemplate(String content) throws Exception {
writeNamedTemplate("controlPanelTemplate.yml", content);
}

private void writeNamedTemplate(String name, String content) throws Exception {
File templateFile = new File(new File("addons/ControlPanel"), name);
java.nio.file.Files.writeString(templateFile.toPath(), content);
}

private ControlPanelObject createDefaultPanel() {
ControlPanelObject cpo = new ControlPanelObject();
cpo.setUniqueId("BSkyBlock_default");
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
da2e5bd
Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot
tastybento May 30, 2026
ad43941
Add files via upload
tastybento Jun 28, 2026
9b41211
ci: add CurseForge publish workflow (publishes the release asset)
tastybento Jun 28, 2026
be4b87a
ci: pin reusable workflow to SHA and enable Hangar publish
tastybento Jun 28, 2026
729f9e7
ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d…
tastybento Jun 28, 2026
96ba6b9
Merge pull request #38 from BentoBoxWorld/ci/curseforge-publish
tastybento Jun 28, 2026
0052c3d
ci: bump pinned reusable workflow to fe4b1f0
tastybento Jul 1, 2026
22c6ad0
Merge pull request #39 from BentoBoxWorld/ci/bump-publish-workflow-fe…
tastybento Jul 1, 2026
6efd980
ci: bump pinned publish-platforms.yml to ca2dcd1
tastybento Jul 3, 2026
693e6d6
Merge pull request #40 from BentoBoxWorld/ci/bump-publish-platforms-c…
tastybento Jul 3, 2026
a3be0bd
Reload re-imports control panels from the template file
tastybento Jul 12, 2026
794cf49
Merge pull request #41 from BentoBoxWorld/fix/reload-reimports-template
tastybento Jul 17, 2026
8f4777e
ci: bump publish-platforms pin to the multipart-JSON fix
tastybento Jul 29, 2026
8deb0fb
Merge pull request #42 from BentoBoxWorld/ci/publish-platforms-pin
tastybento Jul 30, 2026
f2cffa2
Update Chinese translations in zh-TW.yml
qwe664 Aug 4, 2026
bd56c9c
Merge pull request #43 from qwe664/develop
tastybento Aug 4, 2026
ef9c192
Lower minimum BentoBox version to 2.7.1 and bump to 1.16.1
tastybento Aug 31, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
# ControlPanel — .github/workflows/publish.yml
# Publishes the jar attached to a GitHub release to CurseForge via the
# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of
# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar
# githubactions:S7637). workflow_dispatch lets you (re)publish a given version.

name: Publish release to CurseForge

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.2.3)"
required: true
type: string

jobs:
publish:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "" # blank = skip Hangar
curseforge_id: "1514827"
game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5"
version: ${{ inputs.version }} # empty on release events -> falls back to the release tag
secrets:
HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
8 changes: 4 additions & 4 deletions pom.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>1.16.0</build.version>
<build.version>1.16.1</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_ControlPanel</sonar.projectKey>
Expand DownExpand Up@@ -140,9 +140,9 @@
<!-- Your addon must contain Paper and BentoBox APIs dependencies. -->
<dependencies>
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<version>v1.21-SNAPSHOT</version>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.110.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/world/bentobox/controlpanel/ControlPanelAddon.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,19 @@ public void onReload()
}
else
{
// Reload the database into cache first so wipe/re-import operate on a
// consistent view of what is currently stored.
this.manager.reload();

// Re-import each active game mode's panels from controlPanelTemplate.yml so
// that admin edits to the template take effect on reload. Without this the
// panels stay pinned to whatever was first seeded into the database.
this.getPlugin().getAddonsManager().getGameModeAddons().forEach(gameModeAddon -> {
if (!this.settings.getDisabledGameModes().contains(gameModeAddon.getDescription().getName()))
{
this.manager.reimportControlPanels(gameModeAddon);
}
});
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,9 +127,10 @@ public void setup()
@Override
public boolean execute(User user, String label, List<String> args)
{
ControlPanelManager manager = this.<ControlPanelAddon>getAddon().getAddonManager();
ControlPanelAddon addon = this.getAddon();
ControlPanelManager manager = addon.getAddonManager();

String fileName = args.size() == 1 ? args.get(0) : "controlPanelTemplate.yml";
String fileName = args.size() == 1 ? args.get(0) : addon.getSettings().getTemplateFile();

if (manager.hasAnyControlPanel(this.getWorld()))
{
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/world/bentobox/controlpanel/config/Settings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,28 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
}


/**
* This method returns the templateFile value.
*
* @return the value of templateFile.
*/
public String getTemplateFile()
{
return templateFile;
}


/**
* This method sets the templateFile value.
*
* @param templateFile the templateFile new value.
*/
public void setTemplateFile(String templateFile)
{
this.templateFile = templateFile;
}


// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
Expand All@@ -64,4 +86,12 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
@ConfigComment(" - BSkyBlock")
@ConfigEntry(path = "disabled-gamemodes")
private Set<String> disabledGameModes = new HashSet<>();

@ConfigComment("")
@ConfigComment("The template file that control panels are imported from.")
@ConfigComment("This file is read when panels are first seeded and re-read on every reload,")
@ConfigComment("so editing it and running 'bbox reload' will apply your changes.")
@ConfigComment("It must exist in the addon folder (addons/ControlPanel).")
@ConfigEntry(path = "template-file")
private String templateFile = "controlPanelTemplate.yml";
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,8 @@ public ControlPanelManager(ControlPanelAddon addon)
{
this.addon = addon;

// save template file into directory.
// Save the default template file into the directory. The configured template file
// may point elsewhere, but the packaged default is always named controlPanelTemplate.yml.
if (!new File(this.addon.getDataFolder(), "controlPanelTemplate.yml").exists())
{
this.addon.saveResource("controlPanelTemplate.yml", false);
Expand DownExpand Up@@ -128,6 +129,26 @@ public void reload()
}


/**
* This method re-imports the control panels for the given game mode from the
* template file configured via {@code template-file} in config.yml (defaults to
* {@code controlPanelTemplate.yml}), replacing whatever is currently stored in the
* database and cache.
* <p>
* This is what makes edits to the template file take effect on {@code bbox reload}:
* without it, {@link #reload()} only re-reads the database and template changes are
* silently ignored.
*
* @param gameModeAddon Game mode whose panels must be re-imported from the template.
*/
public void reimportControlPanels(@NotNull GameModeAddon gameModeAddon)
{
// Clear existing data so removed/renamed panels do not linger, then re-read the template.
this.wipeData(gameModeAddon, null);
this.importControlPanels(null, gameModeAddon);
}


// ---------------------------------------------------------------------
// Section: Save methods
// ---------------------------------------------------------------------
Expand DownExpand Up@@ -226,7 +247,7 @@ public void importControlPanels(@Nullable User user, @NotNull World world, @NotN
*/
public void importControlPanels(@Nullable User user, @NotNull GameModeAddon addon)
{
this.importControlPanels(user, addon, "controlPanelTemplate");
this.importControlPanels(user, addon, this.addon.getSettings().getTemplateFile());
}


Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/addon.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ repository: 'BentoBoxWorld/ControlPanel'
# Must use Material.values() with uppercase.
icon: 'COMMAND_BLOCK'
# Minimum BentoBox API version
api-version: 3.10.0
api-version: 2.7.1

# List of addon authors.
authors:
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,3 +5,9 @@
# disabled-gamemodes:
# - BSkyBlock
disabled-gamemodes: []
#
# The template file that control panels are imported from.
# This file is read when panels are first seeded and re-read on every reload,
# so editing it and running 'bbox reload' will apply your changes.
# It must exist in the addon folder (addons/ControlPanel).
template-file: controlPanelTemplate.yml
28 changes: 13 additions & 15 deletions src/main/resources/locales/zh-TW.yml
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
---
controlpanel:
messages:
import-count: 導入 [number] 控制面板
control-panels-wiped: "&2 所有為 [gamemode] 模式的控制面板已移除"
import-count: 已匯入 [number] 個控制面板。
control-panels-wiped: '&2已移除 [gamemode] 的所有控制面板。'
errors:
no-load: "&4 無法讀取 [file] 原因: [message]"
no-file: "&4 遺失 [file] 檔案"
no-valid-panels: "&4 未發現任何可用的控制面板"
no-valid-panels-op: "&4 未發現任何可用的控制面板. 你應該試著去導入他 請使用 /[admin] cp import"
not-a-gamemode-world: "&4 未發現任何有效的模式"
no-load: '&4無法載入檔案 [file][message]'
no-file: '&4找不到檔案 [file]。'
not-a-gamemode-world: '&4找不到有效的遊戲模式。'
no-valid-panels: '&4找不到任何有效的控制面板。'
no-valid-panels-op: '&4找不到任何有效的控制面板。請嘗試使用 /[admin] cp import 匯入。'
commands:
admin:
help:
description: '顯示管理員幫助指令

'
description: 顯示管理員說明指令。
import:
parameters: "[file-name] - 要導入的檔案的名子. 檔案為空 所以我們將創建 一般面板."
description: 導入控制面板從 [file]
confirmation: 請確認您要從 [gamemode] 中刪除現有控制面板,並從 [file] 中導入控制面板
parameters: '[file-name] - 要匯入的檔案名稱。留空則匯入預設範本。'
description: [file] 匯入控制面板。
confirmation: 請確認是否要移除 [gamemode] 目前的控制面板,並從 [file] 匯入新的控制面板。
help:
description: 開啟控制面板
parameters: ''
description: 開啟控制面板。
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,4 +35,15 @@ void testSetDisabledGameModes() {
assertTrue(settings.getDisabledGameModes().contains("BSkyBlock"));
assertTrue(settings.getDisabledGameModes().contains("AcidIsland"));
}

@Test
void testDefaultTemplateFile() {
assertEquals("controlPanelTemplate.yml", settings.getTemplateFile());
}

@Test
void testSetTemplateFile() {
settings.setTemplateFile("myPanels.yml");
assertEquals("myPanels.yml", settings.getTemplateFile());
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ public void setUp() throws Exception {

when(addon.getPlugin()).thenReturn(plugin);
when(addon.getLogger()).thenReturn(Logger.getAnonymousLogger());
when(addon.getSettings()).thenReturn(new world.bentobox.controlpanel.config.Settings());

File dataFolder = new File("addons/ControlPanel");
dataFolder.mkdirs();
Expand DownExpand Up@@ -260,6 +261,96 @@ void testReload() {
manager.reload();
}

@Test
void testReimportControlPanelsReadsTemplateFile() throws Exception {
// Make import's trailing getAddonManager().save() call resolve to this manager.
when(addon.getAddonManager()).thenReturn(manager);

// Write a template file that reimport should read from disk.
writeTemplate("""
panel-list:
default:
defaultPanel: true
panelName: 'From Template'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

// Template panel is now in the cache.
assertTrue(manager.hasAnyControlPanel(world));
ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Template", panel.getPanelName());
}

@Test
void testReimportControlPanelsRemovesDeletedPanels() throws Exception {
when(addon.getAddonManager()).thenReturn(manager);

// Seed the cache with an existing panel, as if previously stored in the database.
when(handler.loadObjects()).thenReturn(Collections.singletonList(createDefaultPanel()));
manager.reload();
assertTrue(manager.hasAnyControlPanel(world));

// A template with no panel-list entries should wipe the stored panel on reimport.
writeTemplate("panel-list: {}\n");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

assertFalse(manager.hasAnyControlPanel(world));
}

@Test
void testReimportControlPanelsUsesConfiguredTemplateFile() throws Exception {
// Point the config at a custom template file name.
world.bentobox.controlpanel.config.Settings settings =
new world.bentobox.controlpanel.config.Settings();
settings.setTemplateFile("customPanels.yml");
when(addon.getSettings()).thenReturn(settings);
when(addon.getAddonManager()).thenReturn(manager);

// The default template exists but is empty; only the custom file has a panel.
writeTemplate("panel-list: {}\n");
writeNamedTemplate("customPanels.yml", """
panel-list:
default:
defaultPanel: true
panelName: 'From Custom File'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Custom File", panel.getPanelName());
}

private void writeTemplate(String content) throws Exception {
writeNamedTemplate("controlPanelTemplate.yml", content);
}

private void writeNamedTemplate(String name, String content) throws Exception {
File templateFile = new File(new File("addons/ControlPanel"), name);
java.nio.file.Files.writeString(templateFile.toPath(), content);
}

private ControlPanelObject createDefaultPanel() {
ControlPanelObject cpo = new ControlPanelObject();
cpo.setUniqueId("BSkyBlock_default");
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
da2e5bd
Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot
tastybento May 30, 2026
ad43941
Add files via upload
tastybento Jun 28, 2026
9b41211
ci: add CurseForge publish workflow (publishes the release asset)
tastybento Jun 28, 2026
be4b87a
ci: pin reusable workflow to SHA and enable Hangar publish
tastybento Jun 28, 2026
729f9e7
ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d…
tastybento Jun 28, 2026
96ba6b9
Merge pull request #38 from BentoBoxWorld/ci/curseforge-publish
tastybento Jun 28, 2026
0052c3d
ci: bump pinned reusable workflow to fe4b1f0
tastybento Jul 1, 2026
22c6ad0
Merge pull request #39 from BentoBoxWorld/ci/bump-publish-workflow-fe…
tastybento Jul 1, 2026
6efd980
ci: bump pinned publish-platforms.yml to ca2dcd1
tastybento Jul 3, 2026
693e6d6
Merge pull request #40 from BentoBoxWorld/ci/bump-publish-platforms-c…
tastybento Jul 3, 2026
a3be0bd
Reload re-imports control panels from the template file
tastybento Jul 12, 2026
794cf49
Merge pull request #41 from BentoBoxWorld/fix/reload-reimports-template
tastybento Jul 17, 2026
8f4777e
ci: bump publish-platforms pin to the multipart-JSON fix
tastybento Jul 29, 2026
8deb0fb
Merge pull request #42 from BentoBoxWorld/ci/publish-platforms-pin
tastybento Jul 30, 2026
f2cffa2
Update Chinese translations in zh-TW.yml
qwe664 Aug 4, 2026
bd56c9c
Merge pull request #43 from qwe664/develop
tastybento Aug 4, 2026
ef9c192
Lower minimum BentoBox version to 2.7.1 and bump to 1.16.1
tastybento Aug 31, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
# ControlPanel — .github/workflows/publish.yml
# Publishes the jar attached to a GitHub release to CurseForge via the
# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of
# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar
# githubactions:S7637). workflow_dispatch lets you (re)publish a given version.

name: Publish release to CurseForge

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.2.3)"
required: true
type: string

jobs:
publish:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "" # blank = skip Hangar
curseforge_id: "1514827"
game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5"
version: ${{ inputs.version }} # empty on release events -> falls back to the release tag
secrets:
HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
8 changes: 4 additions & 4 deletions pom.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>1.16.0</build.version>
<build.version>1.16.1</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_ControlPanel</sonar.projectKey>
Expand DownExpand Up@@ -140,9 +140,9 @@
<!-- Your addon must contain Paper and BentoBox APIs dependencies. -->
<dependencies>
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<version>v1.21-SNAPSHOT</version>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.110.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/world/bentobox/controlpanel/ControlPanelAddon.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,19 @@ public void onReload()
}
else
{
// Reload the database into cache first so wipe/re-import operate on a
// consistent view of what is currently stored.
this.manager.reload();

// Re-import each active game mode's panels from controlPanelTemplate.yml so
// that admin edits to the template take effect on reload. Without this the
// panels stay pinned to whatever was first seeded into the database.
this.getPlugin().getAddonsManager().getGameModeAddons().forEach(gameModeAddon -> {
if (!this.settings.getDisabledGameModes().contains(gameModeAddon.getDescription().getName()))
{
this.manager.reimportControlPanels(gameModeAddon);
}
});
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,9 +127,10 @@ public void setup()
@Override
public boolean execute(User user, String label, List<String> args)
{
ControlPanelManager manager = this.<ControlPanelAddon>getAddon().getAddonManager();
ControlPanelAddon addon = this.getAddon();
ControlPanelManager manager = addon.getAddonManager();

String fileName = args.size() == 1 ? args.get(0) : "controlPanelTemplate.yml";
String fileName = args.size() == 1 ? args.get(0) : addon.getSettings().getTemplateFile();

if (manager.hasAnyControlPanel(this.getWorld()))
{
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/world/bentobox/controlpanel/config/Settings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,28 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
}


/**
* This method returns the templateFile value.
*
* @return the value of templateFile.
*/
public String getTemplateFile()
{
return templateFile;
}


/**
* This method sets the templateFile value.
*
* @param templateFile the templateFile new value.
*/
public void setTemplateFile(String templateFile)
{
this.templateFile = templateFile;
}


// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
Expand All@@ -64,4 +86,12 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
@ConfigComment(" - BSkyBlock")
@ConfigEntry(path = "disabled-gamemodes")
private Set<String> disabledGameModes = new HashSet<>();

@ConfigComment("")
@ConfigComment("The template file that control panels are imported from.")
@ConfigComment("This file is read when panels are first seeded and re-read on every reload,")
@ConfigComment("so editing it and running 'bbox reload' will apply your changes.")
@ConfigComment("It must exist in the addon folder (addons/ControlPanel).")
@ConfigEntry(path = "template-file")
private String templateFile = "controlPanelTemplate.yml";
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,8 @@ public ControlPanelManager(ControlPanelAddon addon)
{
this.addon = addon;

// save template file into directory.
// Save the default template file into the directory. The configured template file
// may point elsewhere, but the packaged default is always named controlPanelTemplate.yml.
if (!new File(this.addon.getDataFolder(), "controlPanelTemplate.yml").exists())
{
this.addon.saveResource("controlPanelTemplate.yml", false);
Expand DownExpand Up@@ -128,6 +129,26 @@ public void reload()
}


/**
* This method re-imports the control panels for the given game mode from the
* template file configured via {@code template-file} in config.yml (defaults to
* {@code controlPanelTemplate.yml}), replacing whatever is currently stored in the
* database and cache.
* <p>
* This is what makes edits to the template file take effect on {@code bbox reload}:
* without it, {@link #reload()} only re-reads the database and template changes are
* silently ignored.
*
* @param gameModeAddon Game mode whose panels must be re-imported from the template.
*/
public void reimportControlPanels(@NotNull GameModeAddon gameModeAddon)
{
// Clear existing data so removed/renamed panels do not linger, then re-read the template.
this.wipeData(gameModeAddon, null);
this.importControlPanels(null, gameModeAddon);
}


// ---------------------------------------------------------------------
// Section: Save methods
// ---------------------------------------------------------------------
Expand DownExpand Up@@ -226,7 +247,7 @@ public void importControlPanels(@Nullable User user, @NotNull World world, @NotN
*/
public void importControlPanels(@Nullable User user, @NotNull GameModeAddon addon)
{
this.importControlPanels(user, addon, "controlPanelTemplate");
this.importControlPanels(user, addon, this.addon.getSettings().getTemplateFile());
}


Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/addon.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ repository: 'BentoBoxWorld/ControlPanel'
# Must use Material.values() with uppercase.
icon: 'COMMAND_BLOCK'
# Minimum BentoBox API version
api-version: 3.10.0
api-version: 2.7.1

# List of addon authors.
authors:
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,3 +5,9 @@
# disabled-gamemodes:
# - BSkyBlock
disabled-gamemodes: []
#
# The template file that control panels are imported from.
# This file is read when panels are first seeded and re-read on every reload,
# so editing it and running 'bbox reload' will apply your changes.
# It must exist in the addon folder (addons/ControlPanel).
template-file: controlPanelTemplate.yml
28 changes: 13 additions & 15 deletions src/main/resources/locales/zh-TW.yml
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
---
controlpanel:
messages:
import-count: 導入 [number] 控制面板
control-panels-wiped: "&2 所有為 [gamemode] 模式的控制面板已移除"
import-count: 已匯入 [number] 個控制面板。
control-panels-wiped: '&2已移除 [gamemode] 的所有控制面板。'
errors:
no-load: "&4 無法讀取 [file] 原因: [message]"
no-file: "&4 遺失 [file] 檔案"
no-valid-panels: "&4 未發現任何可用的控制面板"
no-valid-panels-op: "&4 未發現任何可用的控制面板. 你應該試著去導入他 請使用 /[admin] cp import"
not-a-gamemode-world: "&4 未發現任何有效的模式"
no-load: '&4無法載入檔案 [file][message]'
no-file: '&4找不到檔案 [file]。'
not-a-gamemode-world: '&4找不到有效的遊戲模式。'
no-valid-panels: '&4找不到任何有效的控制面板。'
no-valid-panels-op: '&4找不到任何有效的控制面板。請嘗試使用 /[admin] cp import 匯入。'
commands:
admin:
help:
description: '顯示管理員幫助指令

'
description: 顯示管理員說明指令。
import:
parameters: "[file-name] - 要導入的檔案的名子. 檔案為空 所以我們將創建 一般面板."
description: 導入控制面板從 [file]
confirmation: 請確認您要從 [gamemode] 中刪除現有控制面板,並從 [file] 中導入控制面板
parameters: '[file-name] - 要匯入的檔案名稱。留空則匯入預設範本。'
description: [file] 匯入控制面板。
confirmation: 請確認是否要移除 [gamemode] 目前的控制面板,並從 [file] 匯入新的控制面板。
help:
description: 開啟控制面板
parameters: ''
description: 開啟控制面板。
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,4 +35,15 @@ void testSetDisabledGameModes() {
assertTrue(settings.getDisabledGameModes().contains("BSkyBlock"));
assertTrue(settings.getDisabledGameModes().contains("AcidIsland"));
}

@Test
void testDefaultTemplateFile() {
assertEquals("controlPanelTemplate.yml", settings.getTemplateFile());
}

@Test
void testSetTemplateFile() {
settings.setTemplateFile("myPanels.yml");
assertEquals("myPanels.yml", settings.getTemplateFile());
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ public void setUp() throws Exception {

when(addon.getPlugin()).thenReturn(plugin);
when(addon.getLogger()).thenReturn(Logger.getAnonymousLogger());
when(addon.getSettings()).thenReturn(new world.bentobox.controlpanel.config.Settings());

File dataFolder = new File("addons/ControlPanel");
dataFolder.mkdirs();
Expand DownExpand Up@@ -260,6 +261,96 @@ void testReload() {
manager.reload();
}

@Test
void testReimportControlPanelsReadsTemplateFile() throws Exception {
// Make import's trailing getAddonManager().save() call resolve to this manager.
when(addon.getAddonManager()).thenReturn(manager);

// Write a template file that reimport should read from disk.
writeTemplate("""
panel-list:
default:
defaultPanel: true
panelName: 'From Template'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

// Template panel is now in the cache.
assertTrue(manager.hasAnyControlPanel(world));
ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Template", panel.getPanelName());
}

@Test
void testReimportControlPanelsRemovesDeletedPanels() throws Exception {
when(addon.getAddonManager()).thenReturn(manager);

// Seed the cache with an existing panel, as if previously stored in the database.
when(handler.loadObjects()).thenReturn(Collections.singletonList(createDefaultPanel()));
manager.reload();
assertTrue(manager.hasAnyControlPanel(world));

// A template with no panel-list entries should wipe the stored panel on reimport.
writeTemplate("panel-list: {}\n");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

assertFalse(manager.hasAnyControlPanel(world));
}

@Test
void testReimportControlPanelsUsesConfiguredTemplateFile() throws Exception {
// Point the config at a custom template file name.
world.bentobox.controlpanel.config.Settings settings =
new world.bentobox.controlpanel.config.Settings();
settings.setTemplateFile("customPanels.yml");
when(addon.getSettings()).thenReturn(settings);
when(addon.getAddonManager()).thenReturn(manager);

// The default template exists but is empty; only the custom file has a panel.
writeTemplate("panel-list: {}\n");
writeNamedTemplate("customPanels.yml", """
panel-list:
default:
defaultPanel: true
panelName: 'From Custom File'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Custom File", panel.getPanelName());
}

private void writeTemplate(String content) throws Exception {
writeNamedTemplate("controlPanelTemplate.yml", content);
}

private void writeNamedTemplate(String name, String content) throws Exception {
File templateFile = new File(new File("addons/ControlPanel"), name);
java.nio.file.Files.writeString(templateFile.toPath(), content);
}

private ControlPanelObject createDefaultPanel() {
ControlPanelObject cpo = new ControlPanelObject();
cpo.setUniqueId("BSkyBlock_default");
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
da2e5bd
Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot
tastybento May 30, 2026
ad43941
Add files via upload
tastybento Jun 28, 2026
9b41211
ci: add CurseForge publish workflow (publishes the release asset)
tastybento Jun 28, 2026
be4b87a
ci: pin reusable workflow to SHA and enable Hangar publish
tastybento Jun 28, 2026
729f9e7
ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d…
tastybento Jun 28, 2026
96ba6b9
Merge pull request #38 from BentoBoxWorld/ci/curseforge-publish
tastybento Jun 28, 2026
0052c3d
ci: bump pinned reusable workflow to fe4b1f0
tastybento Jul 1, 2026
22c6ad0
Merge pull request #39 from BentoBoxWorld/ci/bump-publish-workflow-fe…
tastybento Jul 1, 2026
6efd980
ci: bump pinned publish-platforms.yml to ca2dcd1
tastybento Jul 3, 2026
693e6d6
Merge pull request #40 from BentoBoxWorld/ci/bump-publish-platforms-c…
tastybento Jul 3, 2026
a3be0bd
Reload re-imports control panels from the template file
tastybento Jul 12, 2026
794cf49
Merge pull request #41 from BentoBoxWorld/fix/reload-reimports-template
tastybento Jul 17, 2026
8f4777e
ci: bump publish-platforms pin to the multipart-JSON fix
tastybento Jul 29, 2026
8deb0fb
Merge pull request #42 from BentoBoxWorld/ci/publish-platforms-pin
tastybento Jul 30, 2026
f2cffa2
Update Chinese translations in zh-TW.yml
qwe664 Aug 4, 2026
bd56c9c
Merge pull request #43 from qwe664/develop
tastybento Aug 4, 2026
ef9c192
Lower minimum BentoBox version to 2.7.1 and bump to 1.16.1
tastybento Aug 31, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
# ControlPanel — .github/workflows/publish.yml
# Publishes the jar attached to a GitHub release to CurseForge via the
# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of
# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar
# githubactions:S7637). workflow_dispatch lets you (re)publish a given version.

name: Publish release to CurseForge

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.2.3)"
required: true
type: string

jobs:
publish:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "" # blank = skip Hangar
curseforge_id: "1514827"
game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5"
version: ${{ inputs.version }} # empty on release events -> falls back to the release tag
secrets:
HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
8 changes: 4 additions & 4 deletions pom.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>1.16.0</build.version>
<build.version>1.16.1</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_ControlPanel</sonar.projectKey>
Expand DownExpand Up@@ -140,9 +140,9 @@
<!-- Your addon must contain Paper and BentoBox APIs dependencies. -->
<dependencies>
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<version>v1.21-SNAPSHOT</version>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.110.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/world/bentobox/controlpanel/ControlPanelAddon.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,19 @@ public void onReload()
}
else
{
// Reload the database into cache first so wipe/re-import operate on a
// consistent view of what is currently stored.
this.manager.reload();

// Re-import each active game mode's panels from controlPanelTemplate.yml so
// that admin edits to the template take effect on reload. Without this the
// panels stay pinned to whatever was first seeded into the database.
this.getPlugin().getAddonsManager().getGameModeAddons().forEach(gameModeAddon -> {
if (!this.settings.getDisabledGameModes().contains(gameModeAddon.getDescription().getName()))
{
this.manager.reimportControlPanels(gameModeAddon);
}
});
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,9 +127,10 @@ public void setup()
@Override
public boolean execute(User user, String label, List<String> args)
{
ControlPanelManager manager = this.<ControlPanelAddon>getAddon().getAddonManager();
ControlPanelAddon addon = this.getAddon();
ControlPanelManager manager = addon.getAddonManager();

String fileName = args.size() == 1 ? args.get(0) : "controlPanelTemplate.yml";
String fileName = args.size() == 1 ? args.get(0) : addon.getSettings().getTemplateFile();

if (manager.hasAnyControlPanel(this.getWorld()))
{
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/world/bentobox/controlpanel/config/Settings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,28 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
}


/**
* This method returns the templateFile value.
*
* @return the value of templateFile.
*/
public String getTemplateFile()
{
return templateFile;
}


/**
* This method sets the templateFile value.
*
* @param templateFile the templateFile new value.
*/
public void setTemplateFile(String templateFile)
{
this.templateFile = templateFile;
}


// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
Expand All@@ -64,4 +86,12 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
@ConfigComment(" - BSkyBlock")
@ConfigEntry(path = "disabled-gamemodes")
private Set<String> disabledGameModes = new HashSet<>();

@ConfigComment("")
@ConfigComment("The template file that control panels are imported from.")
@ConfigComment("This file is read when panels are first seeded and re-read on every reload,")
@ConfigComment("so editing it and running 'bbox reload' will apply your changes.")
@ConfigComment("It must exist in the addon folder (addons/ControlPanel).")
@ConfigEntry(path = "template-file")
private String templateFile = "controlPanelTemplate.yml";
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,8 @@ public ControlPanelManager(ControlPanelAddon addon)
{
this.addon = addon;

// save template file into directory.
// Save the default template file into the directory. The configured template file
// may point elsewhere, but the packaged default is always named controlPanelTemplate.yml.
if (!new File(this.addon.getDataFolder(), "controlPanelTemplate.yml").exists())
{
this.addon.saveResource("controlPanelTemplate.yml", false);
Expand DownExpand Up@@ -128,6 +129,26 @@ public void reload()
}


/**
* This method re-imports the control panels for the given game mode from the
* template file configured via {@code template-file} in config.yml (defaults to
* {@code controlPanelTemplate.yml}), replacing whatever is currently stored in the
* database and cache.
* <p>
* This is what makes edits to the template file take effect on {@code bbox reload}:
* without it, {@link #reload()} only re-reads the database and template changes are
* silently ignored.
*
* @param gameModeAddon Game mode whose panels must be re-imported from the template.
*/
public void reimportControlPanels(@NotNull GameModeAddon gameModeAddon)
{
// Clear existing data so removed/renamed panels do not linger, then re-read the template.
this.wipeData(gameModeAddon, null);
this.importControlPanels(null, gameModeAddon);
}


// ---------------------------------------------------------------------
// Section: Save methods
// ---------------------------------------------------------------------
Expand DownExpand Up@@ -226,7 +247,7 @@ public void importControlPanels(@Nullable User user, @NotNull World world, @NotN
*/
public void importControlPanels(@Nullable User user, @NotNull GameModeAddon addon)
{
this.importControlPanels(user, addon, "controlPanelTemplate");
this.importControlPanels(user, addon, this.addon.getSettings().getTemplateFile());
}


Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/addon.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ repository: 'BentoBoxWorld/ControlPanel'
# Must use Material.values() with uppercase.
icon: 'COMMAND_BLOCK'
# Minimum BentoBox API version
api-version: 3.10.0
api-version: 2.7.1

# List of addon authors.
authors:
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,3 +5,9 @@
# disabled-gamemodes:
# - BSkyBlock
disabled-gamemodes: []
#
# The template file that control panels are imported from.
# This file is read when panels are first seeded and re-read on every reload,
# so editing it and running 'bbox reload' will apply your changes.
# It must exist in the addon folder (addons/ControlPanel).
template-file: controlPanelTemplate.yml
28 changes: 13 additions & 15 deletions src/main/resources/locales/zh-TW.yml
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
---
controlpanel:
messages:
import-count: 導入 [number] 控制面板
control-panels-wiped: "&2 所有為 [gamemode] 模式的控制面板已移除"
import-count: 已匯入 [number] 個控制面板。
control-panels-wiped: '&2已移除 [gamemode] 的所有控制面板。'
errors:
no-load: "&4 無法讀取 [file] 原因: [message]"
no-file: "&4 遺失 [file] 檔案"
no-valid-panels: "&4 未發現任何可用的控制面板"
no-valid-panels-op: "&4 未發現任何可用的控制面板. 你應該試著去導入他 請使用 /[admin] cp import"
not-a-gamemode-world: "&4 未發現任何有效的模式"
no-load: '&4無法載入檔案 [file][message]'
no-file: '&4找不到檔案 [file]。'
not-a-gamemode-world: '&4找不到有效的遊戲模式。'
no-valid-panels: '&4找不到任何有效的控制面板。'
no-valid-panels-op: '&4找不到任何有效的控制面板。請嘗試使用 /[admin] cp import 匯入。'
commands:
admin:
help:
description: '顯示管理員幫助指令

'
description: 顯示管理員說明指令。
import:
parameters: "[file-name] - 要導入的檔案的名子. 檔案為空 所以我們將創建 一般面板."
description: 導入控制面板從 [file]
confirmation: 請確認您要從 [gamemode] 中刪除現有控制面板,並從 [file] 中導入控制面板
parameters: '[file-name] - 要匯入的檔案名稱。留空則匯入預設範本。'
description: [file] 匯入控制面板。
confirmation: 請確認是否要移除 [gamemode] 目前的控制面板,並從 [file] 匯入新的控制面板。
help:
description: 開啟控制面板
parameters: ''
description: 開啟控制面板。
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,4 +35,15 @@ void testSetDisabledGameModes() {
assertTrue(settings.getDisabledGameModes().contains("BSkyBlock"));
assertTrue(settings.getDisabledGameModes().contains("AcidIsland"));
}

@Test
void testDefaultTemplateFile() {
assertEquals("controlPanelTemplate.yml", settings.getTemplateFile());
}

@Test
void testSetTemplateFile() {
settings.setTemplateFile("myPanels.yml");
assertEquals("myPanels.yml", settings.getTemplateFile());
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ public void setUp() throws Exception {

when(addon.getPlugin()).thenReturn(plugin);
when(addon.getLogger()).thenReturn(Logger.getAnonymousLogger());
when(addon.getSettings()).thenReturn(new world.bentobox.controlpanel.config.Settings());

File dataFolder = new File("addons/ControlPanel");
dataFolder.mkdirs();
Expand DownExpand Up@@ -260,6 +261,96 @@ void testReload() {
manager.reload();
}

@Test
void testReimportControlPanelsReadsTemplateFile() throws Exception {
// Make import's trailing getAddonManager().save() call resolve to this manager.
when(addon.getAddonManager()).thenReturn(manager);

// Write a template file that reimport should read from disk.
writeTemplate("""
panel-list:
default:
defaultPanel: true
panelName: 'From Template'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

// Template panel is now in the cache.
assertTrue(manager.hasAnyControlPanel(world));
ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Template", panel.getPanelName());
}

@Test
void testReimportControlPanelsRemovesDeletedPanels() throws Exception {
when(addon.getAddonManager()).thenReturn(manager);

// Seed the cache with an existing panel, as if previously stored in the database.
when(handler.loadObjects()).thenReturn(Collections.singletonList(createDefaultPanel()));
manager.reload();
assertTrue(manager.hasAnyControlPanel(world));

// A template with no panel-list entries should wipe the stored panel on reimport.
writeTemplate("panel-list: {}\n");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

assertFalse(manager.hasAnyControlPanel(world));
}

@Test
void testReimportControlPanelsUsesConfiguredTemplateFile() throws Exception {
// Point the config at a custom template file name.
world.bentobox.controlpanel.config.Settings settings =
new world.bentobox.controlpanel.config.Settings();
settings.setTemplateFile("customPanels.yml");
when(addon.getSettings()).thenReturn(settings);
when(addon.getAddonManager()).thenReturn(manager);

// The default template exists but is empty; only the custom file has a panel.
writeTemplate("panel-list: {}\n");
writeNamedTemplate("customPanels.yml", """
panel-list:
default:
defaultPanel: true
panelName: 'From Custom File'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Custom File", panel.getPanelName());
}

private void writeTemplate(String content) throws Exception {
writeNamedTemplate("controlPanelTemplate.yml", content);
}

private void writeNamedTemplate(String name, String content) throws Exception {
File templateFile = new File(new File("addons/ControlPanel"), name);
java.nio.file.Files.writeString(templateFile.toPath(), content);
}

private ControlPanelObject createDefaultPanel() {
ControlPanelObject cpo = new ControlPanelObject();
cpo.setUniqueId("BSkyBlock_default");
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
da2e5bd
Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot
tastybento May 30, 2026
ad43941
Add files via upload
tastybento Jun 28, 2026
9b41211
ci: add CurseForge publish workflow (publishes the release asset)
tastybento Jun 28, 2026
be4b87a
ci: pin reusable workflow to SHA and enable Hangar publish
tastybento Jun 28, 2026
729f9e7
ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d…
tastybento Jun 28, 2026
96ba6b9
Merge pull request #38 from BentoBoxWorld/ci/curseforge-publish
tastybento Jun 28, 2026
0052c3d
ci: bump pinned reusable workflow to fe4b1f0
tastybento Jul 1, 2026
22c6ad0
Merge pull request #39 from BentoBoxWorld/ci/bump-publish-workflow-fe…
tastybento Jul 1, 2026
6efd980
ci: bump pinned publish-platforms.yml to ca2dcd1
tastybento Jul 3, 2026
693e6d6
Merge pull request #40 from BentoBoxWorld/ci/bump-publish-platforms-c…
tastybento Jul 3, 2026
a3be0bd
Reload re-imports control panels from the template file
tastybento Jul 12, 2026
794cf49
Merge pull request #41 from BentoBoxWorld/fix/reload-reimports-template
tastybento Jul 17, 2026
8f4777e
ci: bump publish-platforms pin to the multipart-JSON fix
tastybento Jul 29, 2026
8deb0fb
Merge pull request #42 from BentoBoxWorld/ci/publish-platforms-pin
tastybento Jul 30, 2026
f2cffa2
Update Chinese translations in zh-TW.yml
qwe664 Aug 4, 2026
bd56c9c
Merge pull request #43 from qwe664/develop
tastybento Aug 4, 2026
ef9c192
Lower minimum BentoBox version to 2.7.1 and bump to 1.16.1
tastybento Aug 31, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
# ControlPanel — .github/workflows/publish.yml
# Publishes the jar attached to a GitHub release to CurseForge via the
# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of
# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar
# githubactions:S7637). workflow_dispatch lets you (re)publish a given version.

name: Publish release to CurseForge

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.2.3)"
required: true
type: string

jobs:
publish:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "" # blank = skip Hangar
curseforge_id: "1514827"
game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5"
version: ${{ inputs.version }} # empty on release events -> falls back to the release tag
secrets:
HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
8 changes: 4 additions & 4 deletions pom.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>1.16.0</build.version>
<build.version>1.16.1</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_ControlPanel</sonar.projectKey>
Expand DownExpand Up@@ -140,9 +140,9 @@
<!-- Your addon must contain Paper and BentoBox APIs dependencies. -->
<dependencies>
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<version>v1.21-SNAPSHOT</version>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.110.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/world/bentobox/controlpanel/ControlPanelAddon.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,19 @@ public void onReload()
}
else
{
// Reload the database into cache first so wipe/re-import operate on a
// consistent view of what is currently stored.
this.manager.reload();

// Re-import each active game mode's panels from controlPanelTemplate.yml so
// that admin edits to the template take effect on reload. Without this the
// panels stay pinned to whatever was first seeded into the database.
this.getPlugin().getAddonsManager().getGameModeAddons().forEach(gameModeAddon -> {
if (!this.settings.getDisabledGameModes().contains(gameModeAddon.getDescription().getName()))
{
this.manager.reimportControlPanels(gameModeAddon);
}
});
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,9 +127,10 @@ public void setup()
@Override
public boolean execute(User user, String label, List<String> args)
{
ControlPanelManager manager = this.<ControlPanelAddon>getAddon().getAddonManager();
ControlPanelAddon addon = this.getAddon();
ControlPanelManager manager = addon.getAddonManager();

String fileName = args.size() == 1 ? args.get(0) : "controlPanelTemplate.yml";
String fileName = args.size() == 1 ? args.get(0) : addon.getSettings().getTemplateFile();

if (manager.hasAnyControlPanel(this.getWorld()))
{
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/world/bentobox/controlpanel/config/Settings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,28 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
}


/**
* This method returns the templateFile value.
*
* @return the value of templateFile.
*/
public String getTemplateFile()
{
return templateFile;
}


/**
* This method sets the templateFile value.
*
* @param templateFile the templateFile new value.
*/
public void setTemplateFile(String templateFile)
{
this.templateFile = templateFile;
}


// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
Expand All@@ -64,4 +86,12 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
@ConfigComment(" - BSkyBlock")
@ConfigEntry(path = "disabled-gamemodes")
private Set<String> disabledGameModes = new HashSet<>();

@ConfigComment("")
@ConfigComment("The template file that control panels are imported from.")
@ConfigComment("This file is read when panels are first seeded and re-read on every reload,")
@ConfigComment("so editing it and running 'bbox reload' will apply your changes.")
@ConfigComment("It must exist in the addon folder (addons/ControlPanel).")
@ConfigEntry(path = "template-file")
private String templateFile = "controlPanelTemplate.yml";
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,8 @@ public ControlPanelManager(ControlPanelAddon addon)
{
this.addon = addon;

// save template file into directory.
// Save the default template file into the directory. The configured template file
// may point elsewhere, but the packaged default is always named controlPanelTemplate.yml.
if (!new File(this.addon.getDataFolder(), "controlPanelTemplate.yml").exists())
{
this.addon.saveResource("controlPanelTemplate.yml", false);
Expand DownExpand Up@@ -128,6 +129,26 @@ public void reload()
}


/**
* This method re-imports the control panels for the given game mode from the
* template file configured via {@code template-file} in config.yml (defaults to
* {@code controlPanelTemplate.yml}), replacing whatever is currently stored in the
* database and cache.
* <p>
* This is what makes edits to the template file take effect on {@code bbox reload}:
* without it, {@link #reload()} only re-reads the database and template changes are
* silently ignored.
*
* @param gameModeAddon Game mode whose panels must be re-imported from the template.
*/
public void reimportControlPanels(@NotNull GameModeAddon gameModeAddon)
{
// Clear existing data so removed/renamed panels do not linger, then re-read the template.
this.wipeData(gameModeAddon, null);
this.importControlPanels(null, gameModeAddon);
}


// ---------------------------------------------------------------------
// Section: Save methods
// ---------------------------------------------------------------------
Expand DownExpand Up@@ -226,7 +247,7 @@ public void importControlPanels(@Nullable User user, @NotNull World world, @NotN
*/
public void importControlPanels(@Nullable User user, @NotNull GameModeAddon addon)
{
this.importControlPanels(user, addon, "controlPanelTemplate");
this.importControlPanels(user, addon, this.addon.getSettings().getTemplateFile());
}


Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/addon.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ repository: 'BentoBoxWorld/ControlPanel'
# Must use Material.values() with uppercase.
icon: 'COMMAND_BLOCK'
# Minimum BentoBox API version
api-version: 3.10.0
api-version: 2.7.1

# List of addon authors.
authors:
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,3 +5,9 @@
# disabled-gamemodes:
# - BSkyBlock
disabled-gamemodes: []
#
# The template file that control panels are imported from.
# This file is read when panels are first seeded and re-read on every reload,
# so editing it and running 'bbox reload' will apply your changes.
# It must exist in the addon folder (addons/ControlPanel).
template-file: controlPanelTemplate.yml
28 changes: 13 additions & 15 deletions src/main/resources/locales/zh-TW.yml
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
---
controlpanel:
messages:
import-count: 導入 [number] 控制面板
control-panels-wiped: "&2 所有為 [gamemode] 模式的控制面板已移除"
import-count: 已匯入 [number] 個控制面板。
control-panels-wiped: '&2已移除 [gamemode] 的所有控制面板。'
errors:
no-load: "&4 無法讀取 [file] 原因: [message]"
no-file: "&4 遺失 [file] 檔案"
no-valid-panels: "&4 未發現任何可用的控制面板"
no-valid-panels-op: "&4 未發現任何可用的控制面板. 你應該試著去導入他 請使用 /[admin] cp import"
not-a-gamemode-world: "&4 未發現任何有效的模式"
no-load: '&4無法載入檔案 [file][message]'
no-file: '&4找不到檔案 [file]。'
not-a-gamemode-world: '&4找不到有效的遊戲模式。'
no-valid-panels: '&4找不到任何有效的控制面板。'
no-valid-panels-op: '&4找不到任何有效的控制面板。請嘗試使用 /[admin] cp import 匯入。'
commands:
admin:
help:
description: '顯示管理員幫助指令

'
description: 顯示管理員說明指令。
import:
parameters: "[file-name] - 要導入的檔案的名子. 檔案為空 所以我們將創建 一般面板."
description: 導入控制面板從 [file]
confirmation: 請確認您要從 [gamemode] 中刪除現有控制面板,並從 [file] 中導入控制面板
parameters: '[file-name] - 要匯入的檔案名稱。留空則匯入預設範本。'
description: [file] 匯入控制面板。
confirmation: 請確認是否要移除 [gamemode] 目前的控制面板,並從 [file] 匯入新的控制面板。
help:
description: 開啟控制面板
parameters: ''
description: 開啟控制面板。
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,4 +35,15 @@ void testSetDisabledGameModes() {
assertTrue(settings.getDisabledGameModes().contains("BSkyBlock"));
assertTrue(settings.getDisabledGameModes().contains("AcidIsland"));
}

@Test
void testDefaultTemplateFile() {
assertEquals("controlPanelTemplate.yml", settings.getTemplateFile());
}

@Test
void testSetTemplateFile() {
settings.setTemplateFile("myPanels.yml");
assertEquals("myPanels.yml", settings.getTemplateFile());
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ public void setUp() throws Exception {

when(addon.getPlugin()).thenReturn(plugin);
when(addon.getLogger()).thenReturn(Logger.getAnonymousLogger());
when(addon.getSettings()).thenReturn(new world.bentobox.controlpanel.config.Settings());

File dataFolder = new File("addons/ControlPanel");
dataFolder.mkdirs();
Expand DownExpand Up@@ -260,6 +261,96 @@ void testReload() {
manager.reload();
}

@Test
void testReimportControlPanelsReadsTemplateFile() throws Exception {
// Make import's trailing getAddonManager().save() call resolve to this manager.
when(addon.getAddonManager()).thenReturn(manager);

// Write a template file that reimport should read from disk.
writeTemplate("""
panel-list:
default:
defaultPanel: true
panelName: 'From Template'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

// Template panel is now in the cache.
assertTrue(manager.hasAnyControlPanel(world));
ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Template", panel.getPanelName());
}

@Test
void testReimportControlPanelsRemovesDeletedPanels() throws Exception {
when(addon.getAddonManager()).thenReturn(manager);

// Seed the cache with an existing panel, as if previously stored in the database.
when(handler.loadObjects()).thenReturn(Collections.singletonList(createDefaultPanel()));
manager.reload();
assertTrue(manager.hasAnyControlPanel(world));

// A template with no panel-list entries should wipe the stored panel on reimport.
writeTemplate("panel-list: {}\n");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

assertFalse(manager.hasAnyControlPanel(world));
}

@Test
void testReimportControlPanelsUsesConfiguredTemplateFile() throws Exception {
// Point the config at a custom template file name.
world.bentobox.controlpanel.config.Settings settings =
new world.bentobox.controlpanel.config.Settings();
settings.setTemplateFile("customPanels.yml");
when(addon.getSettings()).thenReturn(settings);
when(addon.getAddonManager()).thenReturn(manager);

// The default template exists but is empty; only the custom file has a panel.
writeTemplate("panel-list: {}\n");
writeNamedTemplate("customPanels.yml", """
panel-list:
default:
defaultPanel: true
panelName: 'From Custom File'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Custom File", panel.getPanelName());
}

private void writeTemplate(String content) throws Exception {
writeNamedTemplate("controlPanelTemplate.yml", content);
}

private void writeNamedTemplate(String name, String content) throws Exception {
File templateFile = new File(new File("addons/ControlPanel"), name);
java.nio.file.Files.writeString(templateFile.toPath(), content);
}

private ControlPanelObject createDefaultPanel() {
ControlPanelObject cpo = new ControlPanelObject();
cpo.setUniqueId("BSkyBlock_default");
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
da2e5bd
Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot
tastybento May 30, 2026
ad43941
Add files via upload
tastybento Jun 28, 2026
9b41211
ci: add CurseForge publish workflow (publishes the release asset)
tastybento Jun 28, 2026
be4b87a
ci: pin reusable workflow to SHA and enable Hangar publish
tastybento Jun 28, 2026
729f9e7
ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d…
tastybento Jun 28, 2026
96ba6b9
Merge pull request #38 from BentoBoxWorld/ci/curseforge-publish
tastybento Jun 28, 2026
0052c3d
ci: bump pinned reusable workflow to fe4b1f0
tastybento Jul 1, 2026
22c6ad0
Merge pull request #39 from BentoBoxWorld/ci/bump-publish-workflow-fe…
tastybento Jul 1, 2026
6efd980
ci: bump pinned publish-platforms.yml to ca2dcd1
tastybento Jul 3, 2026
693e6d6
Merge pull request #40 from BentoBoxWorld/ci/bump-publish-platforms-c…
tastybento Jul 3, 2026
a3be0bd
Reload re-imports control panels from the template file
tastybento Jul 12, 2026
794cf49
Merge pull request #41 from BentoBoxWorld/fix/reload-reimports-template
tastybento Jul 17, 2026
8f4777e
ci: bump publish-platforms pin to the multipart-JSON fix
tastybento Jul 29, 2026
8deb0fb
Merge pull request #42 from BentoBoxWorld/ci/publish-platforms-pin
tastybento Jul 30, 2026
f2cffa2
Update Chinese translations in zh-TW.yml
qwe664 Aug 4, 2026
bd56c9c
Merge pull request #43 from qwe664/develop
tastybento Aug 4, 2026
ef9c192
Lower minimum BentoBox version to 2.7.1 and bump to 1.16.1
tastybento Aug 31, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
# ControlPanel — .github/workflows/publish.yml
# Publishes the jar attached to a GitHub release to CurseForge via the
# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of
# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar
# githubactions:S7637). workflow_dispatch lets you (re)publish a given version.

name: Publish release to CurseForge

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.2.3)"
required: true
type: string

jobs:
publish:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "" # blank = skip Hangar
curseforge_id: "1514827"
game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5"
version: ${{ inputs.version }} # empty on release events -> falls back to the release tag
secrets:
HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
8 changes: 4 additions & 4 deletions pom.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>1.16.0</build.version>
<build.version>1.16.1</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_ControlPanel</sonar.projectKey>
Expand DownExpand Up@@ -140,9 +140,9 @@
<!-- Your addon must contain Paper and BentoBox APIs dependencies. -->
<dependencies>
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<version>v1.21-SNAPSHOT</version>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.110.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/world/bentobox/controlpanel/ControlPanelAddon.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,19 @@ public void onReload()
}
else
{
// Reload the database into cache first so wipe/re-import operate on a
// consistent view of what is currently stored.
this.manager.reload();

// Re-import each active game mode's panels from controlPanelTemplate.yml so
// that admin edits to the template take effect on reload. Without this the
// panels stay pinned to whatever was first seeded into the database.
this.getPlugin().getAddonsManager().getGameModeAddons().forEach(gameModeAddon -> {
if (!this.settings.getDisabledGameModes().contains(gameModeAddon.getDescription().getName()))
{
this.manager.reimportControlPanels(gameModeAddon);
}
});
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,9 +127,10 @@ public void setup()
@Override
public boolean execute(User user, String label, List<String> args)
{
ControlPanelManager manager = this.<ControlPanelAddon>getAddon().getAddonManager();
ControlPanelAddon addon = this.getAddon();
ControlPanelManager manager = addon.getAddonManager();

String fileName = args.size() == 1 ? args.get(0) : "controlPanelTemplate.yml";
String fileName = args.size() == 1 ? args.get(0) : addon.getSettings().getTemplateFile();

if (manager.hasAnyControlPanel(this.getWorld()))
{
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/world/bentobox/controlpanel/config/Settings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,28 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
}


/**
* This method returns the templateFile value.
*
* @return the value of templateFile.
*/
public String getTemplateFile()
{
return templateFile;
}


/**
* This method sets the templateFile value.
*
* @param templateFile the templateFile new value.
*/
public void setTemplateFile(String templateFile)
{
this.templateFile = templateFile;
}


// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
Expand All@@ -64,4 +86,12 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
@ConfigComment(" - BSkyBlock")
@ConfigEntry(path = "disabled-gamemodes")
private Set<String> disabledGameModes = new HashSet<>();

@ConfigComment("")
@ConfigComment("The template file that control panels are imported from.")
@ConfigComment("This file is read when panels are first seeded and re-read on every reload,")
@ConfigComment("so editing it and running 'bbox reload' will apply your changes.")
@ConfigComment("It must exist in the addon folder (addons/ControlPanel).")
@ConfigEntry(path = "template-file")
private String templateFile = "controlPanelTemplate.yml";
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,8 @@ public ControlPanelManager(ControlPanelAddon addon)
{
this.addon = addon;

// save template file into directory.
// Save the default template file into the directory. The configured template file
// may point elsewhere, but the packaged default is always named controlPanelTemplate.yml.
if (!new File(this.addon.getDataFolder(), "controlPanelTemplate.yml").exists())
{
this.addon.saveResource("controlPanelTemplate.yml", false);
Expand DownExpand Up@@ -128,6 +129,26 @@ public void reload()
}


/**
* This method re-imports the control panels for the given game mode from the
* template file configured via {@code template-file} in config.yml (defaults to
* {@code controlPanelTemplate.yml}), replacing whatever is currently stored in the
* database and cache.
* <p>
* This is what makes edits to the template file take effect on {@code bbox reload}:
* without it, {@link #reload()} only re-reads the database and template changes are
* silently ignored.
*
* @param gameModeAddon Game mode whose panels must be re-imported from the template.
*/
public void reimportControlPanels(@NotNull GameModeAddon gameModeAddon)
{
// Clear existing data so removed/renamed panels do not linger, then re-read the template.
this.wipeData(gameModeAddon, null);
this.importControlPanels(null, gameModeAddon);
}


// ---------------------------------------------------------------------
// Section: Save methods
// ---------------------------------------------------------------------
Expand DownExpand Up@@ -226,7 +247,7 @@ public void importControlPanels(@Nullable User user, @NotNull World world, @NotN
*/
public void importControlPanels(@Nullable User user, @NotNull GameModeAddon addon)
{
this.importControlPanels(user, addon, "controlPanelTemplate");
this.importControlPanels(user, addon, this.addon.getSettings().getTemplateFile());
}


Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/addon.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ repository: 'BentoBoxWorld/ControlPanel'
# Must use Material.values() with uppercase.
icon: 'COMMAND_BLOCK'
# Minimum BentoBox API version
api-version: 3.10.0
api-version: 2.7.1

# List of addon authors.
authors:
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,3 +5,9 @@
# disabled-gamemodes:
# - BSkyBlock
disabled-gamemodes: []
#
# The template file that control panels are imported from.
# This file is read when panels are first seeded and re-read on every reload,
# so editing it and running 'bbox reload' will apply your changes.
# It must exist in the addon folder (addons/ControlPanel).
template-file: controlPanelTemplate.yml
28 changes: 13 additions & 15 deletions src/main/resources/locales/zh-TW.yml
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
---
controlpanel:
messages:
import-count: 導入 [number] 控制面板
control-panels-wiped: "&2 所有為 [gamemode] 模式的控制面板已移除"
import-count: 已匯入 [number] 個控制面板。
control-panels-wiped: '&2已移除 [gamemode] 的所有控制面板。'
errors:
no-load: "&4 無法讀取 [file] 原因: [message]"
no-file: "&4 遺失 [file] 檔案"
no-valid-panels: "&4 未發現任何可用的控制面板"
no-valid-panels-op: "&4 未發現任何可用的控制面板. 你應該試著去導入他 請使用 /[admin] cp import"
not-a-gamemode-world: "&4 未發現任何有效的模式"
no-load: '&4無法載入檔案 [file][message]'
no-file: '&4找不到檔案 [file]。'
not-a-gamemode-world: '&4找不到有效的遊戲模式。'
no-valid-panels: '&4找不到任何有效的控制面板。'
no-valid-panels-op: '&4找不到任何有效的控制面板。請嘗試使用 /[admin] cp import 匯入。'
commands:
admin:
help:
description: '顯示管理員幫助指令

'
description: 顯示管理員說明指令。
import:
parameters: "[file-name] - 要導入的檔案的名子. 檔案為空 所以我們將創建 一般面板."
description: 導入控制面板從 [file]
confirmation: 請確認您要從 [gamemode] 中刪除現有控制面板,並從 [file] 中導入控制面板
parameters: '[file-name] - 要匯入的檔案名稱。留空則匯入預設範本。'
description: [file] 匯入控制面板。
confirmation: 請確認是否要移除 [gamemode] 目前的控制面板,並從 [file] 匯入新的控制面板。
help:
description: 開啟控制面板
parameters: ''
description: 開啟控制面板。
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,4 +35,15 @@ void testSetDisabledGameModes() {
assertTrue(settings.getDisabledGameModes().contains("BSkyBlock"));
assertTrue(settings.getDisabledGameModes().contains("AcidIsland"));
}

@Test
void testDefaultTemplateFile() {
assertEquals("controlPanelTemplate.yml", settings.getTemplateFile());
}

@Test
void testSetTemplateFile() {
settings.setTemplateFile("myPanels.yml");
assertEquals("myPanels.yml", settings.getTemplateFile());
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ public void setUp() throws Exception {

when(addon.getPlugin()).thenReturn(plugin);
when(addon.getLogger()).thenReturn(Logger.getAnonymousLogger());
when(addon.getSettings()).thenReturn(new world.bentobox.controlpanel.config.Settings());

File dataFolder = new File("addons/ControlPanel");
dataFolder.mkdirs();
Expand DownExpand Up@@ -260,6 +261,96 @@ void testReload() {
manager.reload();
}

@Test
void testReimportControlPanelsReadsTemplateFile() throws Exception {
// Make import's trailing getAddonManager().save() call resolve to this manager.
when(addon.getAddonManager()).thenReturn(manager);

// Write a template file that reimport should read from disk.
writeTemplate("""
panel-list:
default:
defaultPanel: true
panelName: 'From Template'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

// Template panel is now in the cache.
assertTrue(manager.hasAnyControlPanel(world));
ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Template", panel.getPanelName());
}

@Test
void testReimportControlPanelsRemovesDeletedPanels() throws Exception {
when(addon.getAddonManager()).thenReturn(manager);

// Seed the cache with an existing panel, as if previously stored in the database.
when(handler.loadObjects()).thenReturn(Collections.singletonList(createDefaultPanel()));
manager.reload();
assertTrue(manager.hasAnyControlPanel(world));

// A template with no panel-list entries should wipe the stored panel on reimport.
writeTemplate("panel-list: {}\n");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

assertFalse(manager.hasAnyControlPanel(world));
}

@Test
void testReimportControlPanelsUsesConfiguredTemplateFile() throws Exception {
// Point the config at a custom template file name.
world.bentobox.controlpanel.config.Settings settings =
new world.bentobox.controlpanel.config.Settings();
settings.setTemplateFile("customPanels.yml");
when(addon.getSettings()).thenReturn(settings);
when(addon.getAddonManager()).thenReturn(manager);

// The default template exists but is empty; only the custom file has a panel.
writeTemplate("panel-list: {}\n");
writeNamedTemplate("customPanels.yml", """
panel-list:
default:
defaultPanel: true
panelName: 'From Custom File'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Custom File", panel.getPanelName());
}

private void writeTemplate(String content) throws Exception {
writeNamedTemplate("controlPanelTemplate.yml", content);
}

private void writeNamedTemplate(String name, String content) throws Exception {
File templateFile = new File(new File("addons/ControlPanel"), name);
java.nio.file.Files.writeString(templateFile.toPath(), content);
}

private ControlPanelObject createDefaultPanel() {
ControlPanelObject cpo = new ControlPanelObject();
cpo.setUniqueId("BSkyBlock_default");
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
da2e5bd
Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot
tastybento May 30, 2026
ad43941
Add files via upload
tastybento Jun 28, 2026
9b41211
ci: add CurseForge publish workflow (publishes the release asset)
tastybento Jun 28, 2026
be4b87a
ci: pin reusable workflow to SHA and enable Hangar publish
tastybento Jun 28, 2026
729f9e7
ci: bump pinned reusable workflow to 71bf927bce32586216baa6995f21852d…
tastybento Jun 28, 2026
96ba6b9
Merge pull request #38 from BentoBoxWorld/ci/curseforge-publish
tastybento Jun 28, 2026
0052c3d
ci: bump pinned reusable workflow to fe4b1f0
tastybento Jul 1, 2026
22c6ad0
Merge pull request #39 from BentoBoxWorld/ci/bump-publish-workflow-fe…
tastybento Jul 1, 2026
6efd980
ci: bump pinned publish-platforms.yml to ca2dcd1
tastybento Jul 3, 2026
693e6d6
Merge pull request #40 from BentoBoxWorld/ci/bump-publish-platforms-c…
tastybento Jul 3, 2026
a3be0bd
Reload re-imports control panels from the template file
tastybento Jul 12, 2026
794cf49
Merge pull request #41 from BentoBoxWorld/fix/reload-reimports-template
tastybento Jul 17, 2026
8f4777e
ci: bump publish-platforms pin to the multipart-JSON fix
tastybento Jul 29, 2026
8deb0fb
Merge pull request #42 from BentoBoxWorld/ci/publish-platforms-pin
tastybento Jul 30, 2026
f2cffa2
Update Chinese translations in zh-TW.yml
qwe664 Aug 4, 2026
bd56c9c
Merge pull request #43 from qwe664/develop
tastybento Aug 4, 2026
ef9c192
Lower minimum BentoBox version to 2.7.1 and bump to 1.16.1
tastybento Aug 31, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
# ControlPanel — .github/workflows/publish.yml
# Publishes the jar attached to a GitHub release to CurseForge via the
# shared BentoBoxWorld/.github reusable workflow. Downloads the release asset instead of
# rebuilding from source. The reusable workflow is pinned to a commit SHA (Sonar
# githubactions:S7637). workflow_dispatch lets you (re)publish a given version.

name: Publish release to CurseForge

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.2.3)"
required: true
type: string

jobs:
publish:
uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master
with:
use_release_asset: "true" # publish the jar attached to the release; do not rebuild
hangar_slug: "" # blank = skip Hangar
curseforge_id: "1514827"
game_versions: "26.2,26.1.2,26.1.1,26.1,1.21.11,1.21.10,1.21.9,1.21.8,1.21.7,1.21.6,1.21.5"
version: ${{ inputs.version }} # empty on release events -> falls back to the release tag
secrets:
HANGAR_API_KEY: ${{ secrets.HANGAR_API_KEY }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
8 changes: 4 additions & 4 deletions pom.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>1.16.0</build.version>
<build.version>1.16.1</build.version>
<build.number>-LOCAL</build.number>
<!-- Sonar Cloud -->
<sonar.projectKey>BentoBoxWorld_ControlPanel</sonar.projectKey>
Expand DownExpand Up@@ -140,9 +140,9 @@
<!-- Your addon must contain Paper and BentoBox APIs dependencies. -->
<dependencies>
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<version>v1.21-SNAPSHOT</version>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.110.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/world/bentobox/controlpanel/ControlPanelAddon.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,19 @@ public void onReload()
}
else
{
// Reload the database into cache first so wipe/re-import operate on a
// consistent view of what is currently stored.
this.manager.reload();

// Re-import each active game mode's panels from controlPanelTemplate.yml so
// that admin edits to the template take effect on reload. Without this the
// panels stay pinned to whatever was first seeded into the database.
this.getPlugin().getAddonsManager().getGameModeAddons().forEach(gameModeAddon -> {
if (!this.settings.getDisabledGameModes().contains(gameModeAddon.getDescription().getName()))
{
this.manager.reimportControlPanels(gameModeAddon);
}
});
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,9 +127,10 @@ public void setup()
@Override
public boolean execute(User user, String label, List<String> args)
{
ControlPanelManager manager = this.<ControlPanelAddon>getAddon().getAddonManager();
ControlPanelAddon addon = this.getAddon();
ControlPanelManager manager = addon.getAddonManager();

String fileName = args.size() == 1 ? args.get(0) : "controlPanelTemplate.yml";
String fileName = args.size() == 1 ? args.get(0) : addon.getSettings().getTemplateFile();

if (manager.hasAnyControlPanel(this.getWorld()))
{
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/world/bentobox/controlpanel/config/Settings.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,28 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
}


/**
* This method returns the templateFile value.
*
* @return the value of templateFile.
*/
public String getTemplateFile()
{
return templateFile;
}


/**
* This method sets the templateFile value.
*
* @param templateFile the templateFile new value.
*/
public void setTemplateFile(String templateFile)
{
this.templateFile = templateFile;
}


// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
Expand All@@ -64,4 +86,12 @@ public void setDisabledGameModes(Set<String> disabledGameModes)
@ConfigComment(" - BSkyBlock")
@ConfigEntry(path = "disabled-gamemodes")
private Set<String> disabledGameModes = new HashSet<>();

@ConfigComment("")
@ConfigComment("The template file that control panels are imported from.")
@ConfigComment("This file is read when panels are first seeded and re-read on every reload,")
@ConfigComment("so editing it and running 'bbox reload' will apply your changes.")
@ConfigComment("It must exist in the addon folder (addons/ControlPanel).")
@ConfigEntry(path = "template-file")
private String templateFile = "controlPanelTemplate.yml";
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,8 @@ public ControlPanelManager(ControlPanelAddon addon)
{
this.addon = addon;

// save template file into directory.
// Save the default template file into the directory. The configured template file
// may point elsewhere, but the packaged default is always named controlPanelTemplate.yml.
if (!new File(this.addon.getDataFolder(), "controlPanelTemplate.yml").exists())
{
this.addon.saveResource("controlPanelTemplate.yml", false);
Expand DownExpand Up@@ -128,6 +129,26 @@ public void reload()
}


/**
* This method re-imports the control panels for the given game mode from the
* template file configured via {@code template-file} in config.yml (defaults to
* {@code controlPanelTemplate.yml}), replacing whatever is currently stored in the
* database and cache.
* <p>
* This is what makes edits to the template file take effect on {@code bbox reload}:
* without it, {@link #reload()} only re-reads the database and template changes are
* silently ignored.
*
* @param gameModeAddon Game mode whose panels must be re-imported from the template.
*/
public void reimportControlPanels(@NotNull GameModeAddon gameModeAddon)
{
// Clear existing data so removed/renamed panels do not linger, then re-read the template.
this.wipeData(gameModeAddon, null);
this.importControlPanels(null, gameModeAddon);
}


// ---------------------------------------------------------------------
// Section: Save methods
// ---------------------------------------------------------------------
Expand DownExpand Up@@ -226,7 +247,7 @@ public void importControlPanels(@Nullable User user, @NotNull World world, @NotN
*/
public void importControlPanels(@Nullable User user, @NotNull GameModeAddon addon)
{
this.importControlPanels(user, addon, "controlPanelTemplate");
this.importControlPanels(user, addon, this.addon.getSettings().getTemplateFile());
}


Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/addon.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ repository: 'BentoBoxWorld/ControlPanel'
# Must use Material.values() with uppercase.
icon: 'COMMAND_BLOCK'
# Minimum BentoBox API version
api-version: 3.10.0
api-version: 2.7.1

# List of addon authors.
authors:
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,3 +5,9 @@
# disabled-gamemodes:
# - BSkyBlock
disabled-gamemodes: []
#
# The template file that control panels are imported from.
# This file is read when panels are first seeded and re-read on every reload,
# so editing it and running 'bbox reload' will apply your changes.
# It must exist in the addon folder (addons/ControlPanel).
template-file: controlPanelTemplate.yml
28 changes: 13 additions & 15 deletions src/main/resources/locales/zh-TW.yml
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
---
controlpanel:
messages:
import-count: 導入 [number] 控制面板
control-panels-wiped: "&2 所有為 [gamemode] 模式的控制面板已移除"
import-count: 已匯入 [number] 個控制面板。
control-panels-wiped: '&2已移除 [gamemode] 的所有控制面板。'
errors:
no-load: "&4 無法讀取 [file] 原因: [message]"
no-file: "&4 遺失 [file] 檔案"
no-valid-panels: "&4 未發現任何可用的控制面板"
no-valid-panels-op: "&4 未發現任何可用的控制面板. 你應該試著去導入他 請使用 /[admin] cp import"
not-a-gamemode-world: "&4 未發現任何有效的模式"
no-load: '&4無法載入檔案 [file][message]'
no-file: '&4找不到檔案 [file]。'
not-a-gamemode-world: '&4找不到有效的遊戲模式。'
no-valid-panels: '&4找不到任何有效的控制面板。'
no-valid-panels-op: '&4找不到任何有效的控制面板。請嘗試使用 /[admin] cp import 匯入。'
commands:
admin:
help:
description: '顯示管理員幫助指令

'
description: 顯示管理員說明指令。
import:
parameters: "[file-name] - 要導入的檔案的名子. 檔案為空 所以我們將創建 一般面板."
description: 導入控制面板從 [file]
confirmation: 請確認您要從 [gamemode] 中刪除現有控制面板,並從 [file] 中導入控制面板
parameters: '[file-name] - 要匯入的檔案名稱。留空則匯入預設範本。'
description: [file] 匯入控制面板。
confirmation: 請確認是否要移除 [gamemode] 目前的控制面板,並從 [file] 匯入新的控制面板。
help:
description: 開啟控制面板
parameters: ''
description: 開啟控制面板。
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,4 +35,15 @@ void testSetDisabledGameModes() {
assertTrue(settings.getDisabledGameModes().contains("BSkyBlock"));
assertTrue(settings.getDisabledGameModes().contains("AcidIsland"));
}

@Test
void testDefaultTemplateFile() {
assertEquals("controlPanelTemplate.yml", settings.getTemplateFile());
}

@Test
void testSetTemplateFile() {
settings.setTemplateFile("myPanels.yml");
assertEquals("myPanels.yml", settings.getTemplateFile());
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ public void setUp() throws Exception {

when(addon.getPlugin()).thenReturn(plugin);
when(addon.getLogger()).thenReturn(Logger.getAnonymousLogger());
when(addon.getSettings()).thenReturn(new world.bentobox.controlpanel.config.Settings());

File dataFolder = new File("addons/ControlPanel");
dataFolder.mkdirs();
Expand DownExpand Up@@ -260,6 +261,96 @@ void testReload() {
manager.reload();
}

@Test
void testReimportControlPanelsReadsTemplateFile() throws Exception {
// Make import's trailing getAddonManager().save() call resolve to this manager.
when(addon.getAddonManager()).thenReturn(manager);

// Write a template file that reimport should read from disk.
writeTemplate("""
panel-list:
default:
defaultPanel: true
panelName: 'From Template'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

// Template panel is now in the cache.
assertTrue(manager.hasAnyControlPanel(world));
ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Template", panel.getPanelName());
}

@Test
void testReimportControlPanelsRemovesDeletedPanels() throws Exception {
when(addon.getAddonManager()).thenReturn(manager);

// Seed the cache with an existing panel, as if previously stored in the database.
when(handler.loadObjects()).thenReturn(Collections.singletonList(createDefaultPanel()));
manager.reload();
assertTrue(manager.hasAnyControlPanel(world));

// A template with no panel-list entries should wipe the stored panel on reimport.
writeTemplate("panel-list: {}\n");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

assertFalse(manager.hasAnyControlPanel(world));
}

@Test
void testReimportControlPanelsUsesConfiguredTemplateFile() throws Exception {
// Point the config at a custom template file name.
world.bentobox.controlpanel.config.Settings settings =
new world.bentobox.controlpanel.config.Settings();
settings.setTemplateFile("customPanels.yml");
when(addon.getSettings()).thenReturn(settings);
when(addon.getAddonManager()).thenReturn(manager);

// The default template exists but is empty; only the custom file has a panel.
writeTemplate("panel-list: {}\n");
writeNamedTemplate("customPanels.yml", """
panel-list:
default:
defaultPanel: true
panelName: 'From Custom File'
permission: 'default'
buttons:
0:
name: 'Island'
material: GRASS_BLOCK
description: 'Go to your island'
command: '[label] go'
""");

GameModeAddon gameModeAddon = mock(GameModeAddon.class);
manager.reimportControlPanels(gameModeAddon);

ControlPanelObject panel = manager.getUserControlPanel(User.getInstance(mockPlayer), world, "bskyblock.");
assertNotNull(panel);
assertEquals("From Custom File", panel.getPanelName());
}

private void writeTemplate(String content) throws Exception {
writeNamedTemplate("controlPanelTemplate.yml", content);
}

private void writeNamedTemplate(String name, String content) throws Exception {
File templateFile = new File(new File("addons/ControlPanel"), name);
java.nio.file.Files.writeString(templateFile.toPath(), content);
}

private ControlPanelObject createDefaultPanel() {
ControlPanelObject cpo = new ControlPanelObject();
cpo.setUniqueId("BSkyBlock_default");
Expand Down
Loading