Compare commits
6 Commits
a90227ff02
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc621d456e | ||
|
|
cfcd591628 | ||
|
|
e395e87c53 | ||
|
|
5c5c35871d | ||
|
|
5d283909db | ||
|
|
8189359e91 |
57
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
# In Gitea, this must match a label advertised by one of your online runners.
|
||||||
|
# Set it in repository variables, for example: GITEA_RUNNER_LABEL=windows or linux_amd64.
|
||||||
|
runs-on: ${{ vars.GITEA_RUNNER_LABEL }}
|
||||||
|
env:
|
||||||
|
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||||
|
BAGTABS_GITEA_TOKEN: ${{ secrets.BAGTABS_GITEA_TOKEN }}
|
||||||
|
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
|
||||||
|
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
|
||||||
|
MODRINTH_PROJECT_ID: ${{ vars.MODRINTH_PROJECT_ID }}
|
||||||
|
CURSEFORGE_PROJECT_ID: ${{ vars.CURSEFORGE_PROJECT_ID }}
|
||||||
|
MINECRAFT_VERSIONS: ${{ vars.MINECRAFT_VERSIONS }}
|
||||||
|
MOD_LOADERS: ${{ vars.MOD_LOADERS }}
|
||||||
|
CURSEFORGE_GAME_VERSION_NAMES: ${{ vars.CURSEFORGE_GAME_VERSION_NAMES }}
|
||||||
|
CURSEFORGE_GAME_VERSION_IDS: ${{ vars.CURSEFORGE_GAME_VERSION_IDS }}
|
||||||
|
RELEASE_ARTIFACT_GLOB: ${{ vars.RELEASE_ARTIFACT_GLOB }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup JDK 21
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "21"
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
|
- name: Build release jar
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if (Test-Path ./gradlew.bat) {
|
||||||
|
./gradlew.bat --no-daemon clean build
|
||||||
|
} else {
|
||||||
|
chmod +x ./gradlew
|
||||||
|
./gradlew --no-daemon clean build
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Publish release artifacts
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if (Get-Command python -ErrorAction SilentlyContinue) {
|
||||||
|
python ./scripts/publish_release.py
|
||||||
|
} elseif (Get-Command python3 -ErrorAction SilentlyContinue) {
|
||||||
|
python3 ./scripts/publish_release.py
|
||||||
|
} else {
|
||||||
|
throw "Python is not available on PATH."
|
||||||
|
}
|
||||||
105
API.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Bag Tabs API
|
||||||
|
|
||||||
|
Bag Tabs exposes a small runtime API so other mods can make their portable storage items fully compatible without needing Bag Tabs to ship a built-in compat layer.
|
||||||
|
|
||||||
|
There are two pieces:
|
||||||
|
|
||||||
|
- Core API: register a bag provider
|
||||||
|
- Optional Curios helper: expose Curios slot tag keys for wearable bags
|
||||||
|
|
||||||
|
## Dependency Setup
|
||||||
|
|
||||||
|
Add Bag Tabs as a compile-time dependency in your development environment.
|
||||||
|
|
||||||
|
Your mod only needs the Bag Tabs jar present at runtime if you want to call the API directly. If you want Bag Tabs compatibility to stay optional, gate registration behind a Bag Tabs loaded check in your own mod.
|
||||||
|
|
||||||
|
## Core API
|
||||||
|
|
||||||
|
Register a provider with:
|
||||||
|
|
||||||
|
`com.trunksbomb.bagtabs.api.BagTabsApi.registerProvider(...)`
|
||||||
|
|
||||||
|
Implement:
|
||||||
|
|
||||||
|
- [BagTabsProvider](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\main\java\com\trunksbomb\bagtabs\api\BagTabsProvider.java)
|
||||||
|
|
||||||
|
Supporting types:
|
||||||
|
|
||||||
|
- [BagTabsIdentity](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\main\java\com\trunksbomb\bagtabs\api\BagTabsIdentity.java)
|
||||||
|
- [BagTabsLocation](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\main\java\com\trunksbomb\bagtabs\api\BagTabsLocation.java)
|
||||||
|
|
||||||
|
### What a provider can do
|
||||||
|
|
||||||
|
- declare whether an `ItemStack` is one of your bags
|
||||||
|
- provide a stable or semi-stable identity for pinning and ordering
|
||||||
|
- open the bag natively
|
||||||
|
- support drag-insert from the carried stack
|
||||||
|
- report whether a menu is the active/open bag menu
|
||||||
|
- report fullness for the fullness indicator
|
||||||
|
|
||||||
|
### Minimal Example
|
||||||
|
|
||||||
|
A concrete copy-and-adapt example lives here:
|
||||||
|
|
||||||
|
- [ExampleBagTabsCompat.java](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\test\java\com\trunksbomb\bagtabs\api\examples\ExampleBagTabsCompat.java)
|
||||||
|
|
||||||
|
## Identity Rules
|
||||||
|
|
||||||
|
Use a stable identity whenever possible.
|
||||||
|
|
||||||
|
Recommended:
|
||||||
|
|
||||||
|
- UUID stored on the item stack
|
||||||
|
- your mod's own saved container id
|
||||||
|
- another durable unique id already used by your bag system
|
||||||
|
|
||||||
|
Fallback only if you truly do not have a unique identifier:
|
||||||
|
|
||||||
|
- `modid + normalized display name`
|
||||||
|
|
||||||
|
That fallback behaves as semi-unique, which means only one matching bag can be pinned at a time.
|
||||||
|
|
||||||
|
## Optional Curios Helper
|
||||||
|
|
||||||
|
If your bag is wearable in Curios, you do not need a second Bag Tabs provider.
|
||||||
|
|
||||||
|
Register the same core provider once, then mark your item as wearable in Curios with the helper constants/tag keys from:
|
||||||
|
|
||||||
|
- [BagTabsCuriosApi](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\main\java\com\trunksbomb\bagtabs\api\curios\BagTabsCuriosApi.java)
|
||||||
|
|
||||||
|
Helpers:
|
||||||
|
|
||||||
|
- `BagTabsCuriosApi.BACK_SLOT`
|
||||||
|
- `BagTabsCuriosApi.BELT_SLOT`
|
||||||
|
- `BagTabsCuriosApi.backTag()`
|
||||||
|
- `BagTabsCuriosApi.beltTag()`
|
||||||
|
- `BagTabsCuriosApi.tagForSlot(...)`
|
||||||
|
|
||||||
|
### Curios Example
|
||||||
|
|
||||||
|
If your item already has a provider registered with the core API, Bag Tabs will use that same provider when the item is found in a Curios slot.
|
||||||
|
|
||||||
|
You only need to make the item wearable in Curios, for example by adding Curios item tags:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"replace": false,
|
||||||
|
"values": [
|
||||||
|
"examplemod:example_bag"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For a back slot tag file:
|
||||||
|
|
||||||
|
`data/curios/tags/item/back.json`
|
||||||
|
|
||||||
|
For a belt slot tag file:
|
||||||
|
|
||||||
|
`data/curios/tags/item/belt.json`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The Bag Tabs API is runtime registration, not a datapack format.
|
||||||
|
- One provider is enough for both normal inventory support and Curios-worn support.
|
||||||
|
- Curios support is optional; the core Bag Tabs API does not require Curios classes.
|
||||||
113
README.md
@@ -1,25 +1,100 @@
|
|||||||
|
# Bag Tabs
|
||||||
|
|
||||||
Installation information
|
Bag Tabs is a NeoForge mod for Minecraft 1.21.1 that adds a tab strip to inventory screens for bags and other portable storage items you are carrying.
|
||||||
=======
|
|
||||||
|
|
||||||
This template repository can be directly cloned to get you started with a new
|
The goal is simple: if you are carrying a bag, you should be able to open it directly from your inventory without moving it into your hand first.
|
||||||
mod. Simply create a new repository cloned from this one, by following the
|
|
||||||
instructions provided by [GitHub](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template).
|
|
||||||
|
|
||||||
Once you have your clone, simply open the repository in the IDE of your choice. The usual recommendation for an IDE is either IntelliJ IDEA or Eclipse.
|
## What It Does
|
||||||
|
|
||||||
If at any point you are missing libraries in your IDE, or you've run into problems you can
|
- Adds a tab bar to supported inventory screens.
|
||||||
run `gradlew --refresh-dependencies` to refresh the local cache. `gradlew clean` to reset everything
|
- Shows one tab for each carried bag or portable storage item the mod recognizes.
|
||||||
{this does not affect your code} and then start the process again.
|
- Checks supported Curios-equipped storage items as well as the normal player inventory.
|
||||||
|
- Lets you click a tab to open that bag directly.
|
||||||
|
- Keeps the tab bar visible while you move between supported bag screens.
|
||||||
|
- Highlights the currently open bag.
|
||||||
|
- Supports drag-and-drop insertion onto supported bag tabs when the bag can accept the carried stack.
|
||||||
|
|
||||||
Mapping Names:
|
## Included Content
|
||||||
============
|
|
||||||
By default, the MDK is configured to use the official mapping names from Mojang for methods and fields
|
|
||||||
in the Minecraft codebase. These names are covered by a specific license. All modders should be aware of this
|
|
||||||
license. For the latest license text, refer to the mapping file itself, or the reference copy here:
|
|
||||||
https://github.com/NeoForged/NeoForm/blob/main/Mojang.md
|
|
||||||
|
|
||||||
Additional Resources:
|
- A built-in Bag Tabs bag item.
|
||||||
==========
|
- 27 inventory slots, like a single chest.
|
||||||
Community Documentation: https://docs.neoforged.net/
|
- Dyeable bag colors.
|
||||||
NeoForged Discord: https://discord.neoforged.net/
|
- Curios-compatible in the back and belt slots when Curios is installed.
|
||||||
|
- Crafting recipe.
|
||||||
|
- Creative tab with the base bag, all dyed variants, and the Bag Namer tool.
|
||||||
|
- JEI entries for the dyed bag variants.
|
||||||
|
|
||||||
|
## Tab Features
|
||||||
|
|
||||||
|
- Bag tabs use the bag item icon for normal mode.
|
||||||
|
- Compact mode replaces the icon with the bag name.
|
||||||
|
- Dyed bags tint their tab background.
|
||||||
|
- Pinned tabs always appear first.
|
||||||
|
- Pinned tabs can be reordered by drag-and-drop.
|
||||||
|
- Tabs can be locked to disable pinning and reordering.
|
||||||
|
- Overflow arrows appear automatically when more tabs exist than can be shown.
|
||||||
|
- Mouse wheel scrolling works over the dock.
|
||||||
|
- Fullness indicators can be shown on tabs and toggled in dock settings.
|
||||||
|
|
||||||
|
## Dock Features
|
||||||
|
|
||||||
|
- Dock to the GUI bottom, top, left, or right.
|
||||||
|
- Dock to the screen bottom, top, left, or right.
|
||||||
|
- Floating horizontal and floating vertical modes.
|
||||||
|
- Per-player, per-world saved settings.
|
||||||
|
- Global default settings plus per-screen overrides.
|
||||||
|
- Remembered scroll position per screen.
|
||||||
|
- Adjustable X/Y offset.
|
||||||
|
- Adjustable max visible tabs.
|
||||||
|
- Optional compact layout.
|
||||||
|
- Optional fullness indicator.
|
||||||
|
- Draggable in-game dock settings popup with live preview.
|
||||||
|
|
||||||
|
## Bag Opening Behavior
|
||||||
|
|
||||||
|
Bag Tabs supports two kinds of bag opening:
|
||||||
|
|
||||||
|
- Direct support for bags with native or compatibility-backed integration.
|
||||||
|
- A fallback hotbar method for unsupported portable storage items that still look like bags or containers.
|
||||||
|
|
||||||
|
When a bag is not fully supported, the tooltip says:
|
||||||
|
|
||||||
|
- `Mod not supported: uses hotbar method to open`
|
||||||
|
|
||||||
|
If no warning is shown, the bag is using native or compatibility-backed support.
|
||||||
|
|
||||||
|
## Current Compatibility
|
||||||
|
|
||||||
|
Bag Tabs currently includes compatibility support for:
|
||||||
|
|
||||||
|
- Bag Tabs bags
|
||||||
|
- Traveler's Backpack
|
||||||
|
- Sophisticated Backpacks
|
||||||
|
- Dank Storage
|
||||||
|
- Curios-equipped storage items are also detected when Curios is installed
|
||||||
|
|
||||||
|
Other portable storage items may still appear as tabs and may still open through the fallback hotbar method.
|
||||||
|
|
||||||
|
## Bag Namer
|
||||||
|
|
||||||
|
Bag Tabs includes a Bag Namer tool that can rename portable storage items.
|
||||||
|
|
||||||
|
- Works on supported bags and inventory-like items.
|
||||||
|
- Uses an in-world style GUI with input and output slots.
|
||||||
|
- Does not close after each rename, so items can be renamed in batches.
|
||||||
|
- Drops its contents if you leave the screen.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- Left-click tab: open bag
|
||||||
|
- Right-click tab: pin or unpin
|
||||||
|
- Drag pinned tab: reorder pinned tabs
|
||||||
|
- Mouse wheel over dock: scroll visible tabs
|
||||||
|
- `\` by default: open last bag
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Bag Tabs is designed to stay attached to existing inventory screens rather than replacing them.
|
||||||
|
- Unsupported bags may behave differently depending on how their mod handles right-click opening.
|
||||||
|
- Some mods may still need dedicated compatibility if they do not expose normal item-use behavior.
|
||||||
|
- Mod authors can integrate directly through the Bag Tabs API described in [API.md](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\API.md).
|
||||||
|
|||||||
BIN
art/mockups/rename_docked_compact.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
art/mockups/rename_docked_normal.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
art/mockups/rename_floating_horizontal_compact.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
art/mockups/rename_floating_horizontal_normal.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
@@ -33,6 +33,9 @@ repositories {
|
|||||||
includeGroup "curse.maven"
|
includeGroup "curse.maven"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
maven {
|
||||||
|
url = uri("https://maven.theillusivec4.top/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
base {
|
base {
|
||||||
@@ -124,7 +127,9 @@ configurations {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Dev-runtime helpers for the client.
|
// Dev-runtime helpers for the client.
|
||||||
|
compileOnly "top.theillusivec4.curios:curios-neoforge:9.5.1+1.21.1:api"
|
||||||
compileOnly "curse.maven:jei-238222:7391682"
|
compileOnly "curse.maven:jei-238222:7391682"
|
||||||
|
localRuntime "top.theillusivec4.curios:curios-neoforge:9.5.1+1.21.1"
|
||||||
localRuntime "curse.maven:jade-324717:6155158"
|
localRuntime "curse.maven:jade-324717:6155158"
|
||||||
localRuntime "curse.maven:jei-238222:7391682"
|
localRuntime "curse.maven:jei-238222:7391682"
|
||||||
localRuntime "curse.maven:travelers-backpack-321117:7485008"
|
localRuntime "curse.maven:travelers-backpack-321117:7485008"
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ mod_id=bagtabs
|
|||||||
# The human-readable display name for the mod.
|
# The human-readable display name for the mod.
|
||||||
mod_name=Bag Tabs
|
mod_name=Bag Tabs
|
||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=All Rights Reserved
|
mod_license=MIT
|
||||||
# The mod version. See https://semver.org/
|
# The mod version. See https://semver.org/
|
||||||
mod_version=1.0.0
|
mod_version=0.1.0
|
||||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||||
# This should match the base package used for the mod sources.
|
# This should match the base package used for the mod sources.
|
||||||
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
||||||
|
|||||||
BIN
project_picture.png
Normal file
|
After Width: | Height: | Size: 798 B |
BIN
project_picture_full.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
327
scripts/generate_tab_mockups.ps1
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$root = Split-Path -Parent $PSScriptRoot
|
||||||
|
$guiDir = Join-Path $root 'src\main\resources\assets\bagtabs\textures\gui'
|
||||||
|
$itemDir = Join-Path $root 'src\main\resources\assets\bagtabs\textures\item'
|
||||||
|
$outDir = Join-Path $root 'art\mockups'
|
||||||
|
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||||||
|
|
||||||
|
function New-Bitmap {
|
||||||
|
param([int]$Width, [int]$Height)
|
||||||
|
return [System.Drawing.Bitmap]::new($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Load-Bitmap {
|
||||||
|
param([string]$Path)
|
||||||
|
return [System.Drawing.Bitmap]::new($Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Half {
|
||||||
|
param(
|
||||||
|
[System.Drawing.Bitmap]$Image,
|
||||||
|
[bool]$RightHalf = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$halfWidth = [int]($Image.Width / 2)
|
||||||
|
$sourceX = if ($RightHalf) { $halfWidth } else { 0 }
|
||||||
|
$bmp = New-Bitmap $halfWidth $Image.Height
|
||||||
|
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
try {
|
||||||
|
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::NearestNeighbor
|
||||||
|
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::Half
|
||||||
|
$graphics.DrawImage(
|
||||||
|
$Image,
|
||||||
|
[System.Drawing.Rectangle]::new(0, 0, $halfWidth, $Image.Height),
|
||||||
|
[System.Drawing.Rectangle]::new($sourceX, 0, $halfWidth, $Image.Height),
|
||||||
|
[System.Drawing.GraphicsUnit]::Pixel
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
$graphics.Dispose()
|
||||||
|
}
|
||||||
|
return $bmp
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tint-Bitmap {
|
||||||
|
param(
|
||||||
|
[System.Drawing.Bitmap]$Image,
|
||||||
|
[Nullable[int]]$Rgb
|
||||||
|
)
|
||||||
|
|
||||||
|
$bmp = New-Bitmap $Image.Width $Image.Height
|
||||||
|
$tr = if ($Rgb.HasValue) { (($Rgb.Value -shr 16) -band 0xFF) / 255.0 } else { 1.0 }
|
||||||
|
$tg = if ($Rgb.HasValue) { (($Rgb.Value -shr 8) -band 0xFF) / 255.0 } else { 1.0 }
|
||||||
|
$tb = if ($Rgb.HasValue) { ($Rgb.Value -band 0xFF) / 255.0 } else { 1.0 }
|
||||||
|
|
||||||
|
for ($x = 0; $x -lt $Image.Width; $x++) {
|
||||||
|
for ($y = 0; $y -lt $Image.Height; $y++) {
|
||||||
|
$src = $Image.GetPixel($x, $y)
|
||||||
|
if ($src.A -eq 0) {
|
||||||
|
$bmp.SetPixel($x, $y, $src)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$r = [Math]::Min(255, [int]($src.R * $tr))
|
||||||
|
$g = [Math]::Min(255, [int]($src.G * $tg))
|
||||||
|
$b = [Math]::Min(255, [int]($src.B * $tb))
|
||||||
|
$bmp.SetPixel($x, $y, [System.Drawing.Color]::FromArgb($src.A, $r, $g, $b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bmp
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compose-Layered {
|
||||||
|
param(
|
||||||
|
[System.Drawing.Bitmap]$Base,
|
||||||
|
[System.Drawing.Bitmap]$Overlay
|
||||||
|
)
|
||||||
|
|
||||||
|
$bmp = New-Bitmap $Base.Width $Base.Height
|
||||||
|
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
try {
|
||||||
|
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::NearestNeighbor
|
||||||
|
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::Half
|
||||||
|
$graphics.DrawImage($Base, 0, 0, $Base.Width, $Base.Height)
|
||||||
|
$graphics.DrawImage($Overlay, 0, 0, $Overlay.Width, $Overlay.Height)
|
||||||
|
} finally {
|
||||||
|
$graphics.Dispose()
|
||||||
|
}
|
||||||
|
return $bmp
|
||||||
|
}
|
||||||
|
|
||||||
|
function Draw-ScaledBitmap {
|
||||||
|
param(
|
||||||
|
[System.Drawing.Graphics]$Graphics,
|
||||||
|
[System.Drawing.Bitmap]$Bitmap,
|
||||||
|
[int]$X,
|
||||||
|
[int]$Y,
|
||||||
|
[int]$Width,
|
||||||
|
[int]$Height
|
||||||
|
)
|
||||||
|
|
||||||
|
$Graphics.DrawImage(
|
||||||
|
$Bitmap,
|
||||||
|
[System.Drawing.Rectangle]::new($X, $Y, $Width, $Height),
|
||||||
|
[System.Drawing.Rectangle]::new(0, 0, $Bitmap.Width, $Bitmap.Height),
|
||||||
|
[System.Drawing.GraphicsUnit]::Pixel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Draw-Pin {
|
||||||
|
param([System.Drawing.Graphics]$Graphics, [int]$X, [int]$Y)
|
||||||
|
$yellow = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 235, 205, 86))
|
||||||
|
$red = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 205, 55, 44))
|
||||||
|
try {
|
||||||
|
$Graphics.FillRectangle($yellow, $X + 1, $Y + 1, 3, 5)
|
||||||
|
$Graphics.FillRectangle($yellow, $X + 2, $Y + 6, 2, 1)
|
||||||
|
$Graphics.FillRectangle($red, $X, $Y + 2, 1, 1)
|
||||||
|
} finally {
|
||||||
|
$yellow.Dispose()
|
||||||
|
$red.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Draw-Fullness {
|
||||||
|
param(
|
||||||
|
[System.Drawing.Graphics]$Graphics,
|
||||||
|
[int]$X,
|
||||||
|
[int]$Y,
|
||||||
|
[int]$Width,
|
||||||
|
[int]$Height,
|
||||||
|
[double]$Percent
|
||||||
|
)
|
||||||
|
|
||||||
|
$back = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 40, 40, 40))
|
||||||
|
$fill = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 105, 180, 90))
|
||||||
|
try {
|
||||||
|
$Graphics.FillRectangle($back, $X, $Y, $Width, $Height)
|
||||||
|
$fillHeight = [Math]::Max(1, [int]([Math]::Round($Height * $Percent)))
|
||||||
|
$Graphics.FillRectangle($fill, $X, $Y + ($Height - $fillHeight), $Width, $fillHeight)
|
||||||
|
} finally {
|
||||||
|
$back.Dispose()
|
||||||
|
$fill.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Draw-BagItem {
|
||||||
|
param(
|
||||||
|
[System.Drawing.Graphics]$Graphics,
|
||||||
|
[System.Drawing.Bitmap]$BagBase,
|
||||||
|
[System.Drawing.Bitmap]$BagOverlay,
|
||||||
|
[Nullable[int]]$Color,
|
||||||
|
[int]$X,
|
||||||
|
[int]$Y,
|
||||||
|
[int]$Size
|
||||||
|
)
|
||||||
|
|
||||||
|
$tinted = Tint-Bitmap $BagBase $Color
|
||||||
|
$bag = Compose-Layered $tinted $BagOverlay
|
||||||
|
try {
|
||||||
|
Draw-ScaledBitmap $Graphics $bag $X $Y $Size $Size
|
||||||
|
} finally {
|
||||||
|
$tinted.Dispose()
|
||||||
|
$bag.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Draw-CompactText {
|
||||||
|
param(
|
||||||
|
[System.Drawing.Graphics]$Graphics,
|
||||||
|
[string]$Line1,
|
||||||
|
[string]$Line2,
|
||||||
|
[int]$X,
|
||||||
|
[int]$Y,
|
||||||
|
[int]$Width,
|
||||||
|
[int]$Height,
|
||||||
|
[System.Drawing.Color]$Color
|
||||||
|
)
|
||||||
|
|
||||||
|
$font = [System.Drawing.Font]::new('Microsoft Sans Serif', 5.5, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Pixel)
|
||||||
|
$brush = [System.Drawing.SolidBrush]::new($Color)
|
||||||
|
$format = [System.Drawing.StringFormat]::new()
|
||||||
|
$format.Alignment = [System.Drawing.StringAlignment]::Center
|
||||||
|
$format.LineAlignment = [System.Drawing.StringAlignment]::Center
|
||||||
|
try {
|
||||||
|
$Graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::SingleBitPerPixelGridFit
|
||||||
|
$Graphics.DrawString($Line1, $font, $brush, [System.Drawing.RectangleF]::new($X, $Y - 1, $Width, [int]($Height / 2)), $format)
|
||||||
|
$Graphics.DrawString($Line2, $font, $brush, [System.Drawing.RectangleF]::new($X, $Y + [int]($Height / 2) - 2, $Width, [int]($Height / 2) + 2), $format)
|
||||||
|
} finally {
|
||||||
|
$font.Dispose()
|
||||||
|
$brush.Dispose()
|
||||||
|
$format.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-Canvas {
|
||||||
|
param([int]$Width, [int]$Height)
|
||||||
|
$bmp = New-Bitmap $Width $Height
|
||||||
|
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::NearestNeighbor
|
||||||
|
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::Half
|
||||||
|
$graphics.Clear([System.Drawing.Color]::FromArgb(255, 48, 48, 52))
|
||||||
|
return @{ Bitmap = $bmp; Graphics = $graphics }
|
||||||
|
}
|
||||||
|
|
||||||
|
$bagNamer = Load-Bitmap (Join-Path $guiDir 'bag_namer.png')
|
||||||
|
$tabBase = Get-Half (Load-Bitmap (Join-Path $guiDir 'bag_tabs_base.png'))
|
||||||
|
$tabOverlay = Get-Half (Load-Bitmap (Join-Path $guiDir 'bag_tabs_overlay.png'))
|
||||||
|
$compactBase = Get-Half (Load-Bitmap (Join-Path $guiDir 'bag_tabs_compact_base.png'))
|
||||||
|
$compactOverlay = Get-Half (Load-Bitmap (Join-Path $guiDir 'bag_tabs_compact_overlay.png'))
|
||||||
|
$dockTab = Load-Bitmap (Join-Path $guiDir 'bag_tabs_dock.png')
|
||||||
|
$gear = Load-Bitmap (Join-Path $guiDir 'dock_gear.png')
|
||||||
|
$lock = Load-Bitmap (Join-Path $guiDir 'dock_lock.png')
|
||||||
|
$prev = Load-Bitmap (Join-Path $guiDir 'dock_prev.png')
|
||||||
|
$next = Load-Bitmap (Join-Path $guiDir 'dock_next.png')
|
||||||
|
$bagBase = Load-Bitmap (Join-Path $itemDir 'bag.png')
|
||||||
|
$bagOverlay = Load-Bitmap (Join-Path $itemDir 'bag_overlay.png')
|
||||||
|
|
||||||
|
$bagColors = @($null, 0x8032A8, 0xE4E5DE)
|
||||||
|
$bagFullness = @(0.82, 0.56, 0.31)
|
||||||
|
$compactNames = @(
|
||||||
|
@{ Line1 = 'Canvas'; Line2 = 'Bag' },
|
||||||
|
@{ Line1 = 'Purple'; Line2 = 'Bag' },
|
||||||
|
@{ Line1 = 'Pale'; Line2 = 'Bag' }
|
||||||
|
)
|
||||||
|
|
||||||
|
function Draw-StandardStrip {
|
||||||
|
param(
|
||||||
|
[System.Drawing.Graphics]$Graphics,
|
||||||
|
[int]$X,
|
||||||
|
[int]$Y,
|
||||||
|
[bool]$Compact
|
||||||
|
)
|
||||||
|
|
||||||
|
Draw-ScaledBitmap $Graphics $dockTab $X $Y 18 22
|
||||||
|
Draw-ScaledBitmap $Graphics $gear ($X + 5) ($Y + 2) 8 8
|
||||||
|
Draw-ScaledBitmap $Graphics $lock ($X + 5) ($Y + 12) 8 8
|
||||||
|
|
||||||
|
$tabStep = 21
|
||||||
|
$tabX = $X + 17
|
||||||
|
for ($i = 0; $i -lt 3; $i++) {
|
||||||
|
if ($Compact) {
|
||||||
|
$tintedBase = Tint-Bitmap $compactBase $bagColors[$i]
|
||||||
|
$tab = Compose-Layered $tintedBase $compactOverlay
|
||||||
|
try {
|
||||||
|
Draw-ScaledBitmap $Graphics $tab ($tabX + ($i * $tabStep)) ($Y + 11) 22 11
|
||||||
|
} finally {
|
||||||
|
$tintedBase.Dispose()
|
||||||
|
$tab.Dispose()
|
||||||
|
}
|
||||||
|
$textColor = if ($bagColors[$i] -eq $null -or $bagColors[$i] -gt 0x7F7F7F) {
|
||||||
|
[System.Drawing.Color]::FromArgb(255, 30, 30, 30)
|
||||||
|
} else {
|
||||||
|
[System.Drawing.Color]::White
|
||||||
|
}
|
||||||
|
Draw-CompactText $Graphics $compactNames[$i].Line1 $compactNames[$i].Line2 ($tabX + ($i * $tabStep) + 2) ($Y + 11) 18 10 $textColor
|
||||||
|
Draw-Fullness $Graphics ($tabX + ($i * $tabStep) + 18) ($Y + 12) 2 8 $bagFullness[$i]
|
||||||
|
} else {
|
||||||
|
$tintedBase = Tint-Bitmap $tabBase $bagColors[$i]
|
||||||
|
$tab = Compose-Layered $tintedBase $tabOverlay
|
||||||
|
try {
|
||||||
|
Draw-ScaledBitmap $Graphics $tab ($tabX + ($i * $tabStep)) $Y 22 22
|
||||||
|
} finally {
|
||||||
|
$tintedBase.Dispose()
|
||||||
|
$tab.Dispose()
|
||||||
|
}
|
||||||
|
Draw-BagItem $Graphics $bagBase $bagOverlay $bagColors[$i] ($tabX + ($i * $tabStep) + 3) ($Y + 2) 16
|
||||||
|
Draw-Fullness $Graphics ($tabX + ($i * $tabStep) + 18) ($Y + 3) 2 15 $bagFullness[$i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Draw-Pin $Graphics ($tabX + 1) ($Y + 1)
|
||||||
|
|
||||||
|
$scrollX = $tabX + (3 * $tabStep)
|
||||||
|
Draw-ScaledBitmap $Graphics $dockTab $scrollX $Y 18 22
|
||||||
|
Draw-ScaledBitmap $Graphics $prev ($scrollX + 5) ($Y + 2) 8 8
|
||||||
|
Draw-ScaledBitmap $Graphics $next ($scrollX + 5) ($Y + 12) 8 8
|
||||||
|
}
|
||||||
|
|
||||||
|
function Save-Mockup {
|
||||||
|
param(
|
||||||
|
[string]$Name,
|
||||||
|
[bool]$Compact,
|
||||||
|
[bool]$Floating
|
||||||
|
)
|
||||||
|
|
||||||
|
$canvas = New-Canvas 260 220
|
||||||
|
$bmp = $canvas.Bitmap
|
||||||
|
$graphics = $canvas.Graphics
|
||||||
|
try {
|
||||||
|
$guiX = 42
|
||||||
|
$guiY = 28
|
||||||
|
Draw-ScaledBitmap $graphics $bagNamer $guiX $guiY $bagNamer.Width $bagNamer.Height
|
||||||
|
|
||||||
|
if ($Floating) {
|
||||||
|
$stripX = $guiX + 18
|
||||||
|
$stripY = $guiY + $bagNamer.Height + 12
|
||||||
|
} else {
|
||||||
|
$stripX = $guiX + 2
|
||||||
|
$stripY = if ($Compact) { $guiY + $bagNamer.Height - 2 } else { $guiY + $bagNamer.Height - 3 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Draw-StandardStrip $graphics $stripX $stripY $Compact
|
||||||
|
$bmp.Save((Join-Path $outDir $Name), [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
} finally {
|
||||||
|
$graphics.Dispose()
|
||||||
|
$bmp.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Save-Mockup 'rename_docked_normal.png' $false $false
|
||||||
|
Save-Mockup 'rename_docked_compact.png' $true $false
|
||||||
|
Save-Mockup 'rename_floating_horizontal_normal.png' $false $true
|
||||||
|
Save-Mockup 'rename_floating_horizontal_compact.png' $true $true
|
||||||
|
|
||||||
|
$bagNamer.Dispose()
|
||||||
|
$tabBase.Dispose()
|
||||||
|
$tabOverlay.Dispose()
|
||||||
|
$compactBase.Dispose()
|
||||||
|
$compactOverlay.Dispose()
|
||||||
|
$dockTab.Dispose()
|
||||||
|
$gear.Dispose()
|
||||||
|
$lock.Dispose()
|
||||||
|
$prev.Dispose()
|
||||||
|
$next.Dispose()
|
||||||
|
$bagBase.Dispose()
|
||||||
|
$bagOverlay.Dispose()
|
||||||
397
scripts/publish_release.py
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
GRADLE_PROPERTIES = ROOT / "gradle.properties"
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message: str) -> None:
|
||||||
|
print(f"ERROR: {message}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def read_gradle_properties() -> dict[str, str]:
|
||||||
|
values: dict[str, str] = {}
|
||||||
|
for line in GRADLE_PROPERTIES.read_text(encoding="utf-8").splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||||
|
continue
|
||||||
|
key, value = stripped.split("=", 1)
|
||||||
|
values[key.strip()] = value.strip()
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def load_release_event() -> dict:
|
||||||
|
event_path = os.environ.get("GITHUB_EVENT_PATH")
|
||||||
|
if not event_path:
|
||||||
|
fail("GITHUB_EVENT_PATH is not set. This script must run inside a release workflow.")
|
||||||
|
with open(event_path, "r", encoding="utf-8") as handle:
|
||||||
|
payload = json.load(handle)
|
||||||
|
if "release" not in payload:
|
||||||
|
fail("Workflow event payload does not contain release data.")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def env_required(name: str) -> str:
|
||||||
|
value = os.environ.get(name, "").strip()
|
||||||
|
if not value:
|
||||||
|
fail(f"Missing required environment value: {name}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def env_optional(name: str, default: str = "") -> str:
|
||||||
|
return os.environ.get(name, default).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def first_non_empty(*values: str) -> str:
|
||||||
|
for value in values:
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def split_csv(value: str) -> list[str]:
|
||||||
|
return [part.strip() for part in value.split(",") if part.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def derive_release_channel(tag_name: str, release_name: str, prerelease: bool) -> str:
|
||||||
|
text = " ".join([tag_name, release_name]).lower()
|
||||||
|
if "alpha" in text:
|
||||||
|
return "alpha"
|
||||||
|
if prerelease or "beta" in text or "rc" in text or "pre" in text:
|
||||||
|
return "beta"
|
||||||
|
return "release"
|
||||||
|
|
||||||
|
|
||||||
|
def select_artifact(mod_id: str, version_hint: str) -> pathlib.Path:
|
||||||
|
pattern = env_optional("RELEASE_ARTIFACT_GLOB", "build/libs/*.jar")
|
||||||
|
candidates = [
|
||||||
|
pathlib.Path(path)
|
||||||
|
for path in glob.glob(str(ROOT / pattern))
|
||||||
|
if pathlib.Path(path).is_file()
|
||||||
|
]
|
||||||
|
filtered = [
|
||||||
|
path
|
||||||
|
for path in candidates
|
||||||
|
if not any(
|
||||||
|
marker in path.name
|
||||||
|
for marker in ("-sources", "-javadoc", "-api", "-dev", "-slim")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if not filtered:
|
||||||
|
fail(f"No release jar matched {pattern}")
|
||||||
|
|
||||||
|
version_name = version_hint[1:] if version_hint.startswith("v") else version_hint
|
||||||
|
exact_name = f"{mod_id}-{version_name}.jar"
|
||||||
|
for path in filtered:
|
||||||
|
if path.name == exact_name:
|
||||||
|
return path
|
||||||
|
|
||||||
|
filtered.sort(key=lambda path: (path.name.count("-"), len(path.name), path.name))
|
||||||
|
return filtered[0]
|
||||||
|
|
||||||
|
|
||||||
|
def http_json(method: str, url: str, headers: dict[str, str]) -> tuple[int, object]:
|
||||||
|
request = urllib.request.Request(url, method=method)
|
||||||
|
for key, value in headers.items():
|
||||||
|
request.add_header(key, value)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request) as response:
|
||||||
|
body = response.read().decode("utf-8")
|
||||||
|
return response.status, json.loads(body) if body else None
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
body = error.read().decode("utf-8", errors="replace")
|
||||||
|
return error.code, json.loads(body) if body else None
|
||||||
|
|
||||||
|
|
||||||
|
def curl_request(arguments: list[str], expected_codes: set[int]) -> str:
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False) as output_file:
|
||||||
|
output_path = output_file.name
|
||||||
|
try:
|
||||||
|
command = [
|
||||||
|
"curl",
|
||||||
|
"--silent",
|
||||||
|
"--show-error",
|
||||||
|
"--location",
|
||||||
|
"--output",
|
||||||
|
output_path,
|
||||||
|
"--write-out",
|
||||||
|
"%{http_code}",
|
||||||
|
*arguments,
|
||||||
|
]
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||||
|
if result.returncode != 0:
|
||||||
|
fail(result.stderr.strip() or "curl failed")
|
||||||
|
status_code = int(result.stdout.strip())
|
||||||
|
body = pathlib.Path(output_path).read_text(encoding="utf-8", errors="replace")
|
||||||
|
if status_code not in expected_codes:
|
||||||
|
fail(f"HTTP {status_code} from remote API:\n{body}")
|
||||||
|
return body
|
||||||
|
finally:
|
||||||
|
pathlib.Path(output_path).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def gitea_headers(token: str) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def attach_to_gitea_release(
|
||||||
|
server_url: str,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
release_id: int,
|
||||||
|
artifact: pathlib.Path,
|
||||||
|
token: str,
|
||||||
|
) -> None:
|
||||||
|
asset_name = artifact.name
|
||||||
|
asset_base = (
|
||||||
|
f"{server_url.rstrip('/')}/api/v1/repos/"
|
||||||
|
f"{urllib.parse.quote(owner)}/{urllib.parse.quote(repo)}/releases/{release_id}/assets"
|
||||||
|
)
|
||||||
|
headers = gitea_headers(token)
|
||||||
|
|
||||||
|
status, payload = http_json("GET", asset_base, headers)
|
||||||
|
if status == 200 and isinstance(payload, list):
|
||||||
|
for asset in payload:
|
||||||
|
if asset.get("name") == asset_name:
|
||||||
|
delete_url = f"{asset_base}/{asset['id']}"
|
||||||
|
delete_status, _ = http_json("DELETE", delete_url, headers)
|
||||||
|
if delete_status not in (204, 404):
|
||||||
|
fail(f"Failed to delete existing Gitea release asset {asset_name}")
|
||||||
|
elif status != 404:
|
||||||
|
fail(f"Could not list Gitea release assets (HTTP {status})")
|
||||||
|
|
||||||
|
upload_url = f"{asset_base}?name={urllib.parse.quote(asset_name)}"
|
||||||
|
print(f"Uploading {asset_name} to Gitea release #{release_id}")
|
||||||
|
curl_request(
|
||||||
|
[
|
||||||
|
"--request",
|
||||||
|
"POST",
|
||||||
|
"--header",
|
||||||
|
f"Authorization: token {token}",
|
||||||
|
"--header",
|
||||||
|
"Accept: application/json",
|
||||||
|
"--form",
|
||||||
|
f"attachment=@{artifact}",
|
||||||
|
upload_url,
|
||||||
|
],
|
||||||
|
{201},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_modrinth(
|
||||||
|
artifact: pathlib.Path,
|
||||||
|
token: str,
|
||||||
|
project_id: str,
|
||||||
|
version_number: str,
|
||||||
|
version_name: str,
|
||||||
|
changelog: str,
|
||||||
|
release_channel: str,
|
||||||
|
game_versions: list[str],
|
||||||
|
loaders: list[str],
|
||||||
|
) -> None:
|
||||||
|
payload = {
|
||||||
|
"project_id": project_id,
|
||||||
|
"version_number": version_number,
|
||||||
|
"version_title": version_name,
|
||||||
|
"version_type": release_channel,
|
||||||
|
"status": "listed",
|
||||||
|
"featured": False,
|
||||||
|
"loaders": loaders,
|
||||||
|
"game_versions": game_versions,
|
||||||
|
"changelog": changelog,
|
||||||
|
"file_parts": ["file"],
|
||||||
|
"primary_file": "file",
|
||||||
|
}
|
||||||
|
print(f"Publishing {artifact.name} to Modrinth project {project_id}")
|
||||||
|
curl_request(
|
||||||
|
[
|
||||||
|
"--request",
|
||||||
|
"POST",
|
||||||
|
"--header",
|
||||||
|
f"Authorization: {token}",
|
||||||
|
"--header",
|
||||||
|
"User-Agent: bagtabs-release-pipeline/1.0 (self-hosted Gitea)",
|
||||||
|
"--form",
|
||||||
|
f"data={json.dumps(payload)}",
|
||||||
|
"--form",
|
||||||
|
f"file=@{artifact}",
|
||||||
|
"https://api.modrinth.com/v2/version",
|
||||||
|
],
|
||||||
|
{200, 201},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_curseforge_game_version_ids(
|
||||||
|
token: str,
|
||||||
|
game_version_names: list[str],
|
||||||
|
explicit_ids: list[str],
|
||||||
|
api_base: str,
|
||||||
|
) -> list[int]:
|
||||||
|
if explicit_ids:
|
||||||
|
return [int(value) for value in explicit_ids]
|
||||||
|
if not game_version_names:
|
||||||
|
fail("Set CURSEFORGE_GAME_VERSION_IDS or CURSEFORGE_GAME_VERSION_NAMES for CurseForge publishing.")
|
||||||
|
|
||||||
|
url = f"{api_base.rstrip('/')}/api/game/versions"
|
||||||
|
request = urllib.request.Request(url, method="GET")
|
||||||
|
request.add_header("X-Api-Token", token)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request) as response:
|
||||||
|
versions = json.loads(response.read().decode("utf-8"))
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
body = error.read().decode("utf-8", errors="replace")
|
||||||
|
fail(f"Could not read CurseForge game versions (HTTP {error.code}):\n{body}")
|
||||||
|
|
||||||
|
matches: list[int] = []
|
||||||
|
missing: list[str] = []
|
||||||
|
for name in game_version_names:
|
||||||
|
matched = next((entry for entry in versions if entry.get("name") == name), None)
|
||||||
|
if not matched:
|
||||||
|
missing.append(name)
|
||||||
|
else:
|
||||||
|
matches.append(int(matched["id"]))
|
||||||
|
if missing:
|
||||||
|
fail(
|
||||||
|
"Could not resolve CurseForge game version names: "
|
||||||
|
+ ", ".join(missing)
|
||||||
|
+ ". Set CURSEFORGE_GAME_VERSION_IDS to bypass lookup."
|
||||||
|
)
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_curseforge(
|
||||||
|
artifact: pathlib.Path,
|
||||||
|
token: str,
|
||||||
|
project_id: str,
|
||||||
|
display_name: str,
|
||||||
|
changelog: str,
|
||||||
|
release_channel: str,
|
||||||
|
game_version_ids: list[int],
|
||||||
|
api_base: str,
|
||||||
|
) -> None:
|
||||||
|
metadata = {
|
||||||
|
"changelog": changelog,
|
||||||
|
"changelogType": "markdown",
|
||||||
|
"displayName": display_name,
|
||||||
|
"gameVersions": game_version_ids,
|
||||||
|
"releaseType": release_channel,
|
||||||
|
}
|
||||||
|
url = f"{api_base.rstrip('/')}/api/projects/{project_id}/upload-file"
|
||||||
|
print(f"Publishing {artifact.name} to CurseForge project {project_id}")
|
||||||
|
curl_request(
|
||||||
|
[
|
||||||
|
"--request",
|
||||||
|
"POST",
|
||||||
|
"--header",
|
||||||
|
f"X-Api-Token: {token}",
|
||||||
|
"--form",
|
||||||
|
f"metadata={json.dumps(metadata)}",
|
||||||
|
"--form",
|
||||||
|
f"file=@{artifact}",
|
||||||
|
url,
|
||||||
|
],
|
||||||
|
{200, 201},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
gradle_properties = read_gradle_properties()
|
||||||
|
event = load_release_event()
|
||||||
|
release = event["release"]
|
||||||
|
repository = event.get("repository", {})
|
||||||
|
repository_full_name = repository.get("full_name", "")
|
||||||
|
repository_parts = repository_full_name.split("/", 1) if "/" in repository_full_name else ["", ""]
|
||||||
|
|
||||||
|
mod_id = gradle_properties["mod_id"]
|
||||||
|
mod_name = gradle_properties["mod_name"]
|
||||||
|
tag_name = release.get("tag_name")
|
||||||
|
if not tag_name:
|
||||||
|
fail("Release tag name missing from event payload.")
|
||||||
|
version_number = tag_name[1:] if tag_name.startswith("v") else tag_name
|
||||||
|
version_name = release.get("name") or f"{mod_name} {version_number}"
|
||||||
|
changelog = release.get("body") or f"Release {version_name}"
|
||||||
|
release_channel = derive_release_channel(tag_name, version_name, bool(release.get("prerelease")))
|
||||||
|
|
||||||
|
artifact = select_artifact(mod_id, tag_name)
|
||||||
|
print(f"Selected artifact: {artifact}")
|
||||||
|
|
||||||
|
server_url = first_non_empty(env_optional("GITEA_SERVER_URL"), env_optional("GITHUB_SERVER_URL"))
|
||||||
|
if not server_url:
|
||||||
|
fail("Could not determine the Gitea server URL from workflow environment.")
|
||||||
|
owner = first_non_empty(
|
||||||
|
repository.get("owner", {}).get("login")
|
||||||
|
or repository.get("owner_name"),
|
||||||
|
repository_parts[0],
|
||||||
|
env_optional("GITEA_REPOSITORY_OWNER"),
|
||||||
|
)
|
||||||
|
repo = first_non_empty(
|
||||||
|
repository.get("name"),
|
||||||
|
repository_parts[1],
|
||||||
|
env_optional("GITEA_REPOSITORY_NAME"),
|
||||||
|
)
|
||||||
|
if not owner or not repo:
|
||||||
|
fail("Could not determine repository owner/name from the release event.")
|
||||||
|
|
||||||
|
attach_to_gitea_release(
|
||||||
|
server_url=server_url,
|
||||||
|
owner=owner,
|
||||||
|
repo=repo,
|
||||||
|
release_id=int(release["id"]),
|
||||||
|
artifact=artifact,
|
||||||
|
token=env_required("BAGTABS_GITEA_TOKEN"),
|
||||||
|
)
|
||||||
|
|
||||||
|
game_versions = split_csv(env_required("MINECRAFT_VERSIONS"))
|
||||||
|
loaders = split_csv(env_required("MOD_LOADERS"))
|
||||||
|
|
||||||
|
upload_to_modrinth(
|
||||||
|
artifact=artifact,
|
||||||
|
token=env_required("MODRINTH_TOKEN"),
|
||||||
|
project_id=env_required("MODRINTH_PROJECT_ID"),
|
||||||
|
version_number=version_number,
|
||||||
|
version_name=version_name,
|
||||||
|
changelog=changelog,
|
||||||
|
release_channel=release_channel,
|
||||||
|
game_versions=game_versions,
|
||||||
|
loaders=loaders,
|
||||||
|
)
|
||||||
|
|
||||||
|
curseforge_token = env_required("CURSEFORGE_TOKEN")
|
||||||
|
curseforge_api_base = env_optional("CURSEFORGE_API_BASE", "https://minecraft.curseforge.com")
|
||||||
|
curseforge_game_version_ids = resolve_curseforge_game_version_ids(
|
||||||
|
token=curseforge_token,
|
||||||
|
game_version_names=split_csv(env_optional("CURSEFORGE_GAME_VERSION_NAMES")),
|
||||||
|
explicit_ids=split_csv(env_optional("CURSEFORGE_GAME_VERSION_IDS")),
|
||||||
|
api_base=curseforge_api_base,
|
||||||
|
)
|
||||||
|
upload_to_curseforge(
|
||||||
|
artifact=artifact,
|
||||||
|
token=curseforge_token,
|
||||||
|
project_id=env_required("CURSEFORGE_PROJECT_ID"),
|
||||||
|
display_name=version_name,
|
||||||
|
changelog=changelog,
|
||||||
|
release_channel=release_channel,
|
||||||
|
game_version_ids=curseforge_game_version_ids,
|
||||||
|
api_base=curseforge_api_base,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Release publishing complete.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
22
src/main/java/com/trunksbomb/bagtabs/api/BagTabsApi.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package com.trunksbomb.bagtabs.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
public final class BagTabsApi {
|
||||||
|
private static final List<BagTabsProvider> PROVIDERS = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
private BagTabsApi() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerProvider(BagTabsProvider provider) {
|
||||||
|
if (provider == null) {
|
||||||
|
throw new IllegalArgumentException("provider cannot be null");
|
||||||
|
}
|
||||||
|
PROVIDERS.add(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<BagTabsProvider> getProviders() {
|
||||||
|
return List.copyOf(PROVIDERS);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.trunksbomb.bagtabs.api;
|
||||||
|
|
||||||
|
public record BagTabsIdentity(String key, String strategy, boolean stable) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.trunksbomb.bagtabs.api;
|
||||||
|
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
public interface BagTabsLocation {
|
||||||
|
ItemStack getStack(Player player);
|
||||||
|
|
||||||
|
boolean setStack(Player player, ItemStack stack);
|
||||||
|
|
||||||
|
boolean moveToHotbar(ServerPlayer player, int hotbarSlot);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.trunksbomb.bagtabs.api;
|
||||||
|
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
public interface BagTabsProvider {
|
||||||
|
boolean supports(ItemStack stack);
|
||||||
|
|
||||||
|
BagTabsIdentity getIdentity(ItemStack stack);
|
||||||
|
|
||||||
|
void open(ServerPlayer player, int slot, BagTabsLocation location, ItemStack stack);
|
||||||
|
|
||||||
|
default boolean canInsertFromCarried(ServerPlayer player, int slot, BagTabsLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean insertFromCarried(ServerPlayer player, int slot, BagTabsLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean isActiveMenu(AbstractContainerMenu menu, Player player, int slot, BagTabsLocation location, ItemStack stack) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default float getFullness(ItemStack stack) {
|
||||||
|
return -1.0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.trunksbomb.bagtabs.api.curios;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.tags.ItemTags;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
|
import net.minecraft.world.item.Item;
|
||||||
|
|
||||||
|
public final class BagTabsCuriosApi {
|
||||||
|
public static final String BACK_SLOT = "back";
|
||||||
|
public static final String BELT_SLOT = "belt";
|
||||||
|
|
||||||
|
private BagTabsCuriosApi() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TagKey<Item> backTag() {
|
||||||
|
return tagForSlot(BACK_SLOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TagKey<Item> beltTag() {
|
||||||
|
return tagForSlot(BELT_SLOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TagKey<Item> tagForSlot(String slotIdentifier) {
|
||||||
|
return ItemTags.create(ResourceLocation.fromNamespaceAndPath("curios", slotIdentifier));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ package com.trunksbomb.bagtabs.bag;
|
|||||||
import com.trunksbomb.bagtabs.item.BagItem;
|
import com.trunksbomb.bagtabs.item.BagItem;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import net.minecraft.world.entity.player.Inventory;
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
@@ -22,10 +25,47 @@ public final class BagAccess {
|
|||||||
}
|
}
|
||||||
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
|
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
|
||||||
if (handler != null || BagCompat.canShowInTabs(stack)) {
|
if (handler != null || BagCompat.canShowInTabs(stack)) {
|
||||||
bags.add(new BagEntry(slot, stack.copy(), handler, BagCompat.getIdentity(stack)));
|
bags.add(new BagEntry(slot, stack.copy(), handler, BagCompat.getIdentity(stack), new InventoryBagLocation(slot)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Integer> usedSyntheticSlots = new HashSet<>();
|
||||||
|
for (BagEntry bag : bags) {
|
||||||
|
usedSyntheticSlots.add(bag.slot());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (CuriosCompat.CuriosSlotRef curiosSlot : CuriosCompat.getCuriosSlots(player)) {
|
||||||
|
ItemStack stack = curiosSlot.stack();
|
||||||
|
if (stack.getItem() instanceof BagItem) {
|
||||||
|
BagIdentityData.ensureBagId(stack);
|
||||||
|
}
|
||||||
|
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
|
||||||
|
if (handler != null || BagCompat.canShowInTabs(stack)) {
|
||||||
|
int syntheticSlot = inventory.getContainerSize()
|
||||||
|
+ 1024
|
||||||
|
+ Math.floorMod(Objects.hash(curiosSlot.identifier(), curiosSlot.slotIndex()), 1_000_000_000);
|
||||||
|
while (!usedSyntheticSlots.add(syntheticSlot)) {
|
||||||
|
syntheticSlot++;
|
||||||
|
}
|
||||||
|
bags.add(new BagEntry(
|
||||||
|
syntheticSlot,
|
||||||
|
stack.copy(),
|
||||||
|
handler,
|
||||||
|
BagCompat.getIdentity(stack),
|
||||||
|
new CuriosBagLocation(curiosSlot.identifier(), curiosSlot.slotIndex())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bags;
|
return bags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static BagEntry findBag(Player player, int slot) {
|
||||||
|
for (BagEntry bag : findBags(player)) {
|
||||||
|
if (bag.slot() == slot) {
|
||||||
|
return bag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.trunksbomb.bagtabs.bag;
|
package com.trunksbomb.bagtabs.bag;
|
||||||
|
|
||||||
|
import com.trunksbomb.bagtabs.api.BagTabsApi;
|
||||||
|
import com.trunksbomb.bagtabs.api.BagTabsIdentity;
|
||||||
|
import com.trunksbomb.bagtabs.api.BagTabsProvider;
|
||||||
import com.trunksbomb.bagtabs.menu.BagMenu;
|
import com.trunksbomb.bagtabs.menu.BagMenu;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
@@ -39,11 +42,17 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (BagTabsProvider provider : BagTabsApi.getProviders()) {
|
||||||
|
if (provider.supports(stack)) {
|
||||||
|
return new ApiBagHandler(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean supportsMenu(AbstractContainerMenu menu) {
|
public static boolean supportsMenu(AbstractContainerMenu menu, net.minecraft.world.entity.player.Player player) {
|
||||||
return getActiveBagSlot(menu) >= 0;
|
return getActiveBagSlot(menu, player) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean canName(ItemStack stack) {
|
public static boolean canName(ItemStack stack) {
|
||||||
@@ -63,6 +72,14 @@ public final class BagCompat {
|
|||||||
return new BagIdentity("bagtabs:uuid:" + BagIdentityData.ensureBagId(stack), "uuid", true);
|
return new BagIdentity("bagtabs:uuid:" + BagIdentityData.ensureBagId(stack), "uuid", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BagHandler handler = findHandler(stack);
|
||||||
|
if (handler instanceof ApiBagHandler apiHandler) {
|
||||||
|
BagTabsIdentity identity = apiHandler.provider().getIdentity(stack);
|
||||||
|
if (identity != null) {
|
||||||
|
return new BagIdentity(identity.key(), identity.strategy(), identity.stable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Integer dankFrequency = getDankFrequency(stack);
|
Integer dankFrequency = getDankFrequency(stack);
|
||||||
if (dankFrequency != null && dankFrequency >= 0) {
|
if (dankFrequency != null && dankFrequency >= 0) {
|
||||||
return new BagIdentity("dankstorage:frequency:" + dankFrequency, "frequency", true);
|
return new BagIdentity("dankstorage:frequency:" + dankFrequency, "frequency", true);
|
||||||
@@ -74,7 +91,7 @@ public final class BagCompat {
|
|||||||
return new BagIdentity(namespace + ":" + normalizedName, "name_mod", false);
|
return new BagIdentity(namespace + ":" + normalizedName, "name_mod", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getActiveBagSlot(AbstractContainerMenu menu) {
|
public static int getActiveBagSlot(AbstractContainerMenu menu, net.minecraft.world.entity.player.Player player) {
|
||||||
if (menu instanceof BagMenu bagMenu) {
|
if (menu instanceof BagMenu bagMenu) {
|
||||||
return bagMenu.getBagSlot();
|
return bagMenu.getBagSlot();
|
||||||
}
|
}
|
||||||
@@ -86,6 +103,14 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (player != null) {
|
||||||
|
for (BagEntry bag : BagAccess.findBags(player)) {
|
||||||
|
if (bag.handler() != null && bag.handler().isActiveMenu(menu, player, bag.slot(), bag.location(), bag.stack())) {
|
||||||
|
return bag.slot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,13 +135,13 @@ public final class BagCompat {
|
|||||||
public interface BagHandler {
|
public interface BagHandler {
|
||||||
boolean supports(ItemStack stack);
|
boolean supports(ItemStack stack);
|
||||||
|
|
||||||
void open(ServerPlayer player, int slot, ItemStack stack);
|
void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack);
|
||||||
|
|
||||||
default boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
default boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
default boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
default boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,11 +149,47 @@ public final class BagCompat {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean isActiveMenu(AbstractContainerMenu menu, net.minecraft.world.entity.player.Player player, int slot, BagLocation location, ItemStack stack) {
|
||||||
|
return this.getActiveBagSlot(menu) == slot;
|
||||||
|
}
|
||||||
|
|
||||||
default float getFullness(ItemStack stack) {
|
default float getFullness(ItemStack stack) {
|
||||||
return -1.0F;
|
return -1.0F;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record ApiBagHandler(BagTabsProvider provider) implements BagHandler {
|
||||||
|
@Override
|
||||||
|
public boolean supports(ItemStack stack) {
|
||||||
|
return this.provider.supports(stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) {
|
||||||
|
this.provider.open(player, slot, location, stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
|
return this.provider.canInsertFromCarried(player, slot, location, bagStack, carriedStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
|
return this.provider.insertFromCarried(player, slot, location, bagStack, carriedStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActiveMenu(AbstractContainerMenu menu, net.minecraft.world.entity.player.Player player, int slot, BagLocation location, ItemStack stack) {
|
||||||
|
return this.provider.isActiveMenu(menu, player, slot, location, stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getFullness(ItemStack stack) {
|
||||||
|
return this.provider.getFullness(stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class NativeBagHandler implements BagHandler {
|
private static final class NativeBagHandler implements BagHandler {
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(ItemStack stack) {
|
public boolean supports(ItemStack stack) {
|
||||||
@@ -136,14 +197,16 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void open(ServerPlayer player, int slot, ItemStack stack) {
|
public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) {
|
||||||
if (stack.getItem() instanceof InventoryBag inventoryBag) {
|
if (stack.getItem() instanceof com.trunksbomb.bagtabs.item.BagItem bagItem) {
|
||||||
|
bagItem.openFromLocation(player, location, slot);
|
||||||
|
} else if (location instanceof InventoryBagLocation && stack.getItem() instanceof InventoryBag inventoryBag) {
|
||||||
inventoryBag.openFromInventory(player, slot);
|
inventoryBag.openFromInventory(player, slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
if (carriedStack.isEmpty() || carriedStack.getItem() instanceof InventoryBag) {
|
if (carriedStack.isEmpty() || carriedStack.getItem() instanceof InventoryBag) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -152,11 +215,11 @@ public final class BagCompat {
|
|||||||
return bagMenu.insertCarriedStack(carriedStack);
|
return bagMenu.insertCarriedStack(carriedStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BagContainer.insertInto(new BagContainer(player, slot), carriedStack);
|
return BagContainer.insertInto(new BagContainer(player, location), carriedStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
if (carriedStack.isEmpty() || carriedStack.getItem() instanceof InventoryBag) {
|
if (carriedStack.isEmpty() || carriedStack.getItem() instanceof InventoryBag) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -165,7 +228,7 @@ public final class BagCompat {
|
|||||||
return bagMenu.canInsertCarriedStack(carriedStack);
|
return bagMenu.canInsertCarriedStack(carriedStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BagContainer.canInsertInto(new BagContainer(player, slot), carriedStack);
|
return BagContainer.canInsertInto(new BagContainer(player, location), carriedStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -199,7 +262,7 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void open(ServerPlayer player, int slot, ItemStack stack) {
|
public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) {
|
||||||
invokeStatic(
|
invokeStatic(
|
||||||
CONTAINER_CLASS,
|
CONTAINER_CLASS,
|
||||||
"openBackpack",
|
"openBackpack",
|
||||||
@@ -227,7 +290,7 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
if (carriedStack.isEmpty()) {
|
if (carriedStack.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -237,7 +300,7 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
if (carriedStack.isEmpty()) {
|
if (carriedStack.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -284,7 +347,7 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void open(ServerPlayer player, int slot, ItemStack stack) {
|
public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) {
|
||||||
try {
|
try {
|
||||||
Method createProvider = stack.getItem().getClass().getMethod("createProvider", ItemStack.class);
|
Method createProvider = stack.getItem().getClass().getMethod("createProvider", ItemStack.class);
|
||||||
Object provider = createProvider.invoke(stack.getItem(), stack);
|
Object provider = createProvider.invoke(stack.getItem(), stack);
|
||||||
@@ -314,7 +377,7 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
if (carriedStack.isEmpty() || ItemStack.isSameItemSameComponents(carriedStack, bagStack)) {
|
if (carriedStack.isEmpty() || ItemStack.isSameItemSameComponents(carriedStack, bagStack)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -324,7 +387,7 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
if (carriedStack.isEmpty() || ItemStack.isSameItemSameComponents(carriedStack, bagStack)) {
|
if (carriedStack.isEmpty() || ItemStack.isSameItemSameComponents(carriedStack, bagStack)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -410,7 +473,7 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void open(ServerPlayer player, int slot, ItemStack stack) {
|
public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) {
|
||||||
try {
|
try {
|
||||||
Class<?> payloadClass = Class.forName(PAYLOAD_CLASS);
|
Class<?> payloadClass = Class.forName(PAYLOAD_CLASS);
|
||||||
Method handlePayload = payloadClass.getMethod(
|
Method handlePayload = payloadClass.getMethod(
|
||||||
@@ -441,7 +504,7 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
if (carriedStack.isEmpty()) {
|
if (carriedStack.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -451,7 +514,7 @@ public final class BagCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) {
|
public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) {
|
||||||
if (carriedStack.isEmpty()) {
|
if (carriedStack.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import net.minecraft.world.item.component.ItemContainerContents;
|
|||||||
|
|
||||||
public class BagContainer extends SimpleContainer {
|
public class BagContainer extends SimpleContainer {
|
||||||
private final Player player;
|
private final Player player;
|
||||||
private final int slot;
|
private final BagLocation location;
|
||||||
|
|
||||||
public BagContainer(Player player, int slot) {
|
public BagContainer(Player player, BagLocation location) {
|
||||||
super(BagItem.SLOT_COUNT);
|
super(BagItem.SLOT_COUNT);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.slot = slot;
|
this.location = location;
|
||||||
|
|
||||||
ItemContainerContents contents = player.getInventory().getItem(slot).getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY);
|
ItemContainerContents contents = location.getStack(player).getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY);
|
||||||
contents.copyInto(this.getItems());
|
contents.copyInto(this.getItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,12 +29,14 @@ public class BagContainer extends SimpleContainer {
|
|||||||
@Override
|
@Override
|
||||||
public void setChanged() {
|
public void setChanged() {
|
||||||
super.setChanged();
|
super.setChanged();
|
||||||
ItemStack bagStack = this.player.getInventory().getItem(this.slot);
|
ItemStack bagStack = this.location.getStack(this.player).copy();
|
||||||
if (!bagStack.isEmpty() && bagStack.getItem() instanceof BagItem) {
|
if (!bagStack.isEmpty() && bagStack.getItem() instanceof BagItem) {
|
||||||
bagStack.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(this.copyItems()));
|
bagStack.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(this.copyItems()));
|
||||||
|
if (this.location.setStack(this.player, bagStack)) {
|
||||||
this.player.getInventory().setChanged();
|
this.player.getInventory().setChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean stillValid(Player player) {
|
public boolean stillValid(Player player) {
|
||||||
@@ -42,7 +44,7 @@ public class BagContainer extends SimpleContainer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack current = player.getInventory().getItem(this.slot);
|
ItemStack current = this.location.getStack(player);
|
||||||
return current.getItem() instanceof BagItem;
|
return current.getItem() instanceof BagItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ package com.trunksbomb.bagtabs.bag;
|
|||||||
|
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
public record BagEntry(int slot, ItemStack stack, BagCompat.BagHandler handler, BagIdentity identity) {
|
public record BagEntry(int slot, ItemStack stack, BagCompat.BagHandler handler, BagIdentity identity, BagLocation location) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.trunksbomb.bagtabs.bag;
|
||||||
|
|
||||||
|
import com.trunksbomb.bagtabs.api.BagTabsLocation;
|
||||||
|
|
||||||
|
public interface BagLocation extends BagTabsLocation {
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.trunksbomb.bagtabs.bag;
|
||||||
|
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
public record CuriosBagLocation(String identifier, int slotIndex) implements BagLocation {
|
||||||
|
@Override
|
||||||
|
public ItemStack getStack(Player player) {
|
||||||
|
return CuriosCompat.getStack(player, this.identifier, this.slotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setStack(Player player, ItemStack stack) {
|
||||||
|
return CuriosCompat.setStack(player, this.identifier, this.slotIndex, stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean moveToHotbar(ServerPlayer player, int hotbarSlot) {
|
||||||
|
return CuriosCompat.moveToHotbar(player, this.identifier, this.slotIndex, hotbarSlot);
|
||||||
|
}
|
||||||
|
}
|
||||||
183
src/main/java/com/trunksbomb/bagtabs/bag/CuriosCompat.java
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package com.trunksbomb.bagtabs.bag;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
public final class CuriosCompat {
|
||||||
|
private static final String API_CLASS = "top.theillusivec4.curios.api.CuriosApi";
|
||||||
|
|
||||||
|
private CuriosCompat() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<CuriosSlotRef> getCuriosSlots(Player player) {
|
||||||
|
Object curiosInventory = getCuriosInventory(player);
|
||||||
|
if (curiosInventory == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object curiosMapObject = invoke(curiosInventory, "getCurios");
|
||||||
|
if (!(curiosMapObject instanceof Map<?, ?> curiosMap)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CuriosSlotRef> slots = new ArrayList<>();
|
||||||
|
curiosMap.entrySet().stream()
|
||||||
|
.sorted(Comparator.comparing(entry -> String.valueOf(entry.getKey())))
|
||||||
|
.forEach(entry -> addSlots(slots, String.valueOf(entry.getKey()), entry.getValue()));
|
||||||
|
return slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAvailable() {
|
||||||
|
try {
|
||||||
|
Class.forName(API_CLASS);
|
||||||
|
return true;
|
||||||
|
} catch (ClassNotFoundException exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemStack getStack(Player player, String identifier, int slotIndex) {
|
||||||
|
Object stacks = getCurioStacks(player, identifier);
|
||||||
|
if (stacks == null) {
|
||||||
|
return ItemStack.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object stack = tryInvoke(stacks, "getStackInSlot", new Class<?>[] {int.class}, slotIndex);
|
||||||
|
return stack instanceof ItemStack itemStack ? itemStack : ItemStack.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean setStack(Player player, String identifier, int slotIndex, ItemStack stack) {
|
||||||
|
Object stacks = getCurioStacks(player, identifier);
|
||||||
|
if (stacks == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!invokeVoid(stacks, "setStackInSlot", new Class<?>[] {int.class, ItemStack.class}, slotIndex, stack.copy())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object handler = getCurioHandler(player, identifier);
|
||||||
|
if (handler != null) {
|
||||||
|
tryInvoke(handler, "setChanged");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean moveToHotbar(ServerPlayer player, String identifier, int slotIndex, int hotbarSlot) {
|
||||||
|
ItemStack sourceStack = getStack(player, identifier, slotIndex).copy();
|
||||||
|
if (sourceStack.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack targetStack = player.getInventory().getItem(hotbarSlot).copy();
|
||||||
|
if (!setStack(player, identifier, slotIndex, targetStack)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.getInventory().setItem(hotbarSlot, sourceStack);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addSlots(List<CuriosSlotRef> slots, String identifier, Object curiosHandler) {
|
||||||
|
Object stacks = tryInvoke(curiosHandler, "getStacks");
|
||||||
|
if (stacks == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer slotCount = getSlotCount(curiosHandler, stacks);
|
||||||
|
if (slotCount == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int slotIndex = 0; slotIndex < slotCount; slotIndex++) {
|
||||||
|
Object stack = tryInvoke(stacks, "getStackInSlot", new Class<?>[] {int.class}, slotIndex);
|
||||||
|
if (stack instanceof ItemStack itemStack && !itemStack.isEmpty()) {
|
||||||
|
slots.add(new CuriosSlotRef(identifier, slotIndex, itemStack));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer getSlotCount(Object curiosHandler, Object stacks) {
|
||||||
|
Object count = tryInvoke(curiosHandler, "getSlots");
|
||||||
|
if (!(count instanceof Integer)) {
|
||||||
|
count = tryInvoke(stacks, "getSlots");
|
||||||
|
}
|
||||||
|
return count instanceof Integer integer ? integer : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object getCuriosInventory(Player player) {
|
||||||
|
try {
|
||||||
|
Class<?> curiosApi = Class.forName(API_CLASS);
|
||||||
|
Method method = curiosApi.getMethod("getCuriosInventory", LivingEntity.class);
|
||||||
|
Object result = method.invoke(null, player);
|
||||||
|
if (result instanceof Optional<?> optional) {
|
||||||
|
return optional.orElse(null);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object getCurioHandler(Player player, String identifier) {
|
||||||
|
Object curiosInventory = getCuriosInventory(player);
|
||||||
|
if (curiosInventory == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object curiosMapObject = invoke(curiosInventory, "getCurios");
|
||||||
|
if (!(curiosMapObject instanceof Map<?, ?> curiosMap)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return curiosMap.get(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object getCurioStacks(Player player, String identifier) {
|
||||||
|
Object handler = getCurioHandler(player, identifier);
|
||||||
|
return handler == null ? null : tryInvoke(handler, "getStacks");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object invoke(Object target, String methodName) {
|
||||||
|
Object result = tryInvoke(target, methodName);
|
||||||
|
if (result == null) {
|
||||||
|
throw new RuntimeException("Failed to invoke " + methodName + " on " + target.getClass().getName());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object tryInvoke(Object target, String methodName, Class<?>[] parameterTypes, Object... args) {
|
||||||
|
try {
|
||||||
|
Method method = target.getClass().getMethod(methodName, parameterTypes);
|
||||||
|
return method.invoke(target, args);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object tryInvoke(Object target, String methodName) {
|
||||||
|
return tryInvoke(target, methodName, new Class<?>[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean invokeVoid(Object target, String methodName, Class<?>[] parameterTypes, Object... args) {
|
||||||
|
try {
|
||||||
|
Method method = target.getClass().getMethod(methodName, parameterTypes);
|
||||||
|
method.invoke(target, args);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CuriosSlotRef(String identifier, int slotIndex, ItemStack stack) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.trunksbomb.bagtabs.bag;
|
||||||
|
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
public record InventoryBagLocation(int slot) implements BagLocation {
|
||||||
|
@Override
|
||||||
|
public ItemStack getStack(Player player) {
|
||||||
|
return player.getInventory().getItem(this.slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setStack(Player player, ItemStack stack) {
|
||||||
|
player.getInventory().setItem(this.slot, stack);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean moveToHotbar(ServerPlayer player, int hotbarSlot) {
|
||||||
|
Inventory inventory = player.getInventory();
|
||||||
|
if (this.slot == hotbarSlot) {
|
||||||
|
return !inventory.getItem(hotbarSlot).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack sourceStack = inventory.getItem(this.slot).copy();
|
||||||
|
if (sourceStack.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack hotbarStack = inventory.getItem(hotbarSlot).copy();
|
||||||
|
inventory.setItem(hotbarSlot, sourceStack);
|
||||||
|
inventory.setItem(this.slot, hotbarStack);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,9 @@ public final class BagTabOverlay {
|
|||||||
private static final ResourceLocation PREV_ICON_TEXTURE = BagTabs.id("textures/gui/dock_prev.png");
|
private static final ResourceLocation PREV_ICON_TEXTURE = BagTabs.id("textures/gui/dock_prev.png");
|
||||||
private static final ResourceLocation NEXT_ICON_TEXTURE = BagTabs.id("textures/gui/dock_next.png");
|
private static final ResourceLocation NEXT_ICON_TEXTURE = BagTabs.id("textures/gui/dock_next.png");
|
||||||
private static final ResourceLocation POPUP_TEXTURE = BagTabs.id("textures/gui/dock_config_popup.png");
|
private static final ResourceLocation POPUP_TEXTURE = BagTabs.id("textures/gui/dock_config_popup.png");
|
||||||
|
private static final ResourceLocation BOOKMARK_TEXTURE = BagTabs.id("textures/gui/bookmark.png");
|
||||||
|
private static final ResourceLocation NORMAL_FILL_GAUGE_TEXTURE = BagTabs.id("textures/gui/normal_fill_gauge.png");
|
||||||
|
private static final ResourceLocation NORMAL_ACTIVE_TAB_TEXTURE = BagTabs.id("textures/gui/normal_active_tab.png");
|
||||||
private static final int BASE_TAB_WIDTH = 22;
|
private static final int BASE_TAB_WIDTH = 22;
|
||||||
private static final int BASE_TAB_HEIGHT = 22;
|
private static final int BASE_TAB_HEIGHT = 22;
|
||||||
private static final int BASE_COMPACT_TAB_WIDTH = 22;
|
private static final int BASE_COMPACT_TAB_WIDTH = 22;
|
||||||
@@ -59,6 +62,12 @@ public final class BagTabOverlay {
|
|||||||
private static final int POPUP_WIDTH = 140;
|
private static final int POPUP_WIDTH = 140;
|
||||||
private static final int POPUP_HEIGHT = 160;
|
private static final int POPUP_HEIGHT = 160;
|
||||||
private static final int POPUP_TITLE_HEIGHT = 16;
|
private static final int POPUP_TITLE_HEIGHT = 16;
|
||||||
|
private static final int BOOKMARK_WIDTH = 3;
|
||||||
|
private static final int BOOKMARK_HEIGHT = 7;
|
||||||
|
private static final int NORMAL_FILL_GAUGE_WIDTH = 2;
|
||||||
|
private static final int NORMAL_FILL_GAUGE_HEIGHT = 18;
|
||||||
|
private static final int NORMAL_ACTIVE_TAB_WIDTH = 20;
|
||||||
|
private static final int NORMAL_ACTIVE_TAB_HEIGHT = 4;
|
||||||
private static final int TAB_GAP = 0;
|
private static final int TAB_GAP = 0;
|
||||||
private static final int TAB_Y_OFFSET = -3;
|
private static final int TAB_Y_OFFSET = -3;
|
||||||
private static final int TAB_X_OFFSET = -6;
|
private static final int TAB_X_OFFSET = -6;
|
||||||
@@ -545,14 +554,15 @@ public final class BagTabOverlay {
|
|||||||
int uOffset = (hovered || selected) ? baseWidth : 0;
|
int uOffset = (hovered || selected) ? baseWidth : 0;
|
||||||
ResourceLocation baseTexture = tab.baseTexture();
|
ResourceLocation baseTexture = tab.baseTexture();
|
||||||
ResourceLocation overlayTexture = tab.overlayTexture();
|
ResourceLocation overlayTexture = tab.overlayTexture();
|
||||||
|
final float tintRed = red;
|
||||||
|
final float tintGreen = green;
|
||||||
|
final float tintBlue = blue;
|
||||||
|
|
||||||
RenderSystem.setShaderColor(red, green, blue, 1.0F);
|
withTabLocalSpace(g, tab, () -> {
|
||||||
blitRotated(g, baseTexture, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, baseWidth, baseHeight, baseWidth * 2, baseHeight, tab.rotationDegrees());
|
RenderSystem.setShaderColor(tintRed, tintGreen, tintBlue, 1.0F);
|
||||||
|
blitTabLocalTexture(g, baseTexture, uOffset, 0, baseWidth, baseHeight);
|
||||||
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||||
blitRotated(g, overlayTexture, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, baseWidth, baseHeight, baseWidth * 2, baseHeight, tab.rotationDegrees());
|
blitTabLocalTexture(g, overlayTexture, uOffset, 0, baseWidth, baseHeight);
|
||||||
|
|
||||||
g.pose().pushPose();
|
|
||||||
pushTabTransform(g, tab);
|
|
||||||
if (dragged || tab.usesFloatingArt()) {
|
if (dragged || tab.usesFloatingArt()) {
|
||||||
g.fill(1, 0, baseWidth - 1, 1, 0xFF000000);
|
g.fill(1, 0, baseWidth - 1, 1, 0xFF000000);
|
||||||
if (baseHeight > 2) {
|
if (baseHeight > 2) {
|
||||||
@@ -569,7 +579,7 @@ public final class BagTabOverlay {
|
|||||||
renderXIndicator(g, baseWidth - 7, Math.min(3, Math.max(0, baseHeight - 6)));
|
renderXIndicator(g, baseWidth - 7, Math.min(3, Math.max(0, baseHeight - 6)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
g.pose().popPose();
|
});
|
||||||
|
|
||||||
if (tab.showFullnessIndicator() && tab.fullness() >= 0.0F) {
|
if (tab.showFullnessIndicator() && tab.fullness() >= 0.0F) {
|
||||||
renderFullnessIndicator(g, tab, tab.fullness());
|
renderFullnessIndicator(g, tab, tab.fullness());
|
||||||
@@ -581,11 +591,12 @@ public final class BagTabOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean supportsTabs(AbstractContainerScreen<?> screen) {
|
private static boolean supportsTabs(AbstractContainerScreen<?> screen) {
|
||||||
return screen instanceof net.minecraft.client.gui.screens.inventory.InventoryScreen || BagCompat.supportsMenu(screen.getMenu());
|
return screen instanceof net.minecraft.client.gui.screens.inventory.InventoryScreen
|
||||||
|
|| BagCompat.supportsMenu(screen.getMenu(), net.minecraft.client.Minecraft.getInstance().player);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getActiveBagSlot(AbstractContainerScreen<?> screen) {
|
private static int getActiveBagSlot(AbstractContainerScreen<?> screen) {
|
||||||
return BagCompat.getActiveBagSlot(screen.getMenu());
|
return BagCompat.getActiveBagSlot(screen.getMenu(), net.minecraft.client.Minecraft.getInstance().player);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void refreshInsertTargets(AbstractContainerScreen<?> screen, ItemStack carried) {
|
private static void refreshInsertTargets(AbstractContainerScreen<?> screen, ItemStack carried) {
|
||||||
@@ -628,6 +639,11 @@ public final class BagTabOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void renderFullnessIndicator(GuiGraphics g, RenderedTab tab, float fullness) {
|
private static void renderFullnessIndicator(GuiGraphics g, RenderedTab tab, float fullness) {
|
||||||
|
if (!tab.compact()) {
|
||||||
|
renderNormalFullnessIndicator(g, tab, fullness);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int color = interpolateColor(0xFF65D96D, 0xFFD94A4A, fullness);
|
int color = interpolateColor(0xFF65D96D, 0xFFD94A4A, fullness);
|
||||||
if (tab.isVertical()) {
|
if (tab.isVertical()) {
|
||||||
int trackHeight = tab.height() <= 11 ? 1 : 2;
|
int trackHeight = tab.height() <= 11 ? 1 : 2;
|
||||||
@@ -658,6 +674,64 @@ public final class BagTabOverlay {
|
|||||||
g.fill(trackX + trackWidth, trackY, trackX + trackWidth + 1, trackY + trackHeight, 0xFF000000);
|
g.fill(trackX + trackWidth, trackY, trackX + trackWidth + 1, trackY + trackHeight, 0xFF000000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void renderNormalFullnessIndicator(GuiGraphics g, RenderedTab tab, float fullness) {
|
||||||
|
int color = interpolateColor(0xFF6CCBFF, 0xFF2E86FF, fullness);
|
||||||
|
withTabLocalSpace(g, tab, () -> {
|
||||||
|
int usableHeight = NORMAL_FILL_GAUGE_HEIGHT - 2;
|
||||||
|
int fillHeight = Math.max(0, Math.min(usableHeight, Math.round(usableHeight * fullness)));
|
||||||
|
|
||||||
|
if (tab.isVertical()) {
|
||||||
|
int gaugeX = Math.max(0, (tab.baseTextureWidth() - NORMAL_FILL_GAUGE_HEIGHT) / 2);
|
||||||
|
int gaugeY = tab.baseTextureHeight() - NORMAL_FILL_GAUGE_WIDTH - 1;
|
||||||
|
LocalElementTransform transform = LocalElementTransform.identity()
|
||||||
|
.translate(gaugeX, gaugeY)
|
||||||
|
.rotate(-90.0F);
|
||||||
|
withLocalElementTransform(g, 0, 0, NORMAL_FILL_GAUGE_WIDTH, NORMAL_FILL_GAUGE_HEIGHT, transform, () -> {
|
||||||
|
g.blit(
|
||||||
|
NORMAL_FILL_GAUGE_TEXTURE,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
NORMAL_FILL_GAUGE_WIDTH,
|
||||||
|
NORMAL_FILL_GAUGE_HEIGHT,
|
||||||
|
NORMAL_FILL_GAUGE_WIDTH,
|
||||||
|
NORMAL_FILL_GAUGE_HEIGHT
|
||||||
|
);
|
||||||
|
if (fillHeight > 0) {
|
||||||
|
int fillTop = 1 + (usableHeight - fillHeight);
|
||||||
|
g.fill(1, fillTop, 2, 1 + usableHeight, color);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int gaugeX = tab.baseTextureWidth() - NORMAL_FILL_GAUGE_WIDTH - 1;
|
||||||
|
int gaugeY = isFlippedHorizontalShell(tab) ? tab.baseTextureHeight() - NORMAL_FILL_GAUGE_HEIGHT : 0;
|
||||||
|
LocalElementTransform transform = LocalElementTransform.identity().translate(gaugeX, gaugeY);
|
||||||
|
if (isFlippedHorizontalShell(tab)) {
|
||||||
|
transform = transform.mirrorVertical();
|
||||||
|
}
|
||||||
|
withLocalElementTransform(g, 0, 0, NORMAL_FILL_GAUGE_WIDTH, NORMAL_FILL_GAUGE_HEIGHT, transform, () -> {
|
||||||
|
g.blit(
|
||||||
|
NORMAL_FILL_GAUGE_TEXTURE,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
NORMAL_FILL_GAUGE_WIDTH,
|
||||||
|
NORMAL_FILL_GAUGE_HEIGHT,
|
||||||
|
NORMAL_FILL_GAUGE_WIDTH,
|
||||||
|
NORMAL_FILL_GAUGE_HEIGHT
|
||||||
|
);
|
||||||
|
if (fillHeight > 0) {
|
||||||
|
int fillTop = 1 + (usableHeight - fillHeight);
|
||||||
|
g.fill(1, fillTop, 2, 1 + usableHeight, color);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static int interpolateColor(int start, int end, float t) {
|
private static int interpolateColor(int start, int end, float t) {
|
||||||
float clamped = Math.max(0.0F, Math.min(1.0F, t));
|
float clamped = Math.max(0.0F, Math.min(1.0F, t));
|
||||||
int sr = (start >> 16) & 0xFF;
|
int sr = (start >> 16) & 0xFF;
|
||||||
@@ -981,18 +1055,53 @@ public final class BagTabOverlay {
|
|||||||
if (!tab.pinned()) {
|
if (!tab.pinned()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int x = tab.x() + 2;
|
|
||||||
int y = tab.y() + 2;
|
if (!tab.compact()) {
|
||||||
g.fill(x, y, x + 4, y + 1, 0xFFFFDA6B);
|
withTabLocalSpace(g, tab, () -> {
|
||||||
g.fill(x + 1, y + 1, x + 3, y + 4, 0xFFFFDA6B);
|
LocalElementTransform transform = LocalElementTransform.identity();
|
||||||
g.fill(x + 2, y + 4, x + 3, y + 5, 0xFFD94A4A);
|
if (!tab.isVertical()) {
|
||||||
|
transform = transform.translate(1, isFlippedHorizontalShell(tab) ? tab.baseTextureHeight() - BOOKMARK_HEIGHT : 0);
|
||||||
|
if (isFlippedHorizontalShell(tab)) {
|
||||||
|
transform = transform.mirrorVertical();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transform = transform.translate(1, 0);
|
||||||
|
}
|
||||||
|
withLocalElementTransform(g, 0, 0, BOOKMARK_WIDTH, BOOKMARK_HEIGHT, transform, () -> {
|
||||||
|
g.blit(
|
||||||
|
BOOKMARK_TEXTURE,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
BOOKMARK_WIDTH,
|
||||||
|
BOOKMARK_HEIGHT,
|
||||||
|
BOOKMARK_WIDTH,
|
||||||
|
BOOKMARK_HEIGHT
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
withTabLocalSpace(g, tab, () -> blitScaled(g, BOOKMARK_TEXTURE, 1, 0, 2, 5, 0, 0, BOOKMARK_WIDTH, BOOKMARK_HEIGHT, BOOKMARK_WIDTH, BOOKMARK_HEIGHT));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void renderSelectedIndicator(GuiGraphics g, RenderedTab tab, int baseWidth, int baseHeight) {
|
private static void renderSelectedIndicator(GuiGraphics g, RenderedTab tab, int baseWidth, int baseHeight) {
|
||||||
if (!tab.compact()) {
|
if (!tab.compact()) {
|
||||||
int highlightTop = Math.max(baseHeight - 3, 1);
|
int activeX = Math.max(0, (baseWidth - NORMAL_ACTIVE_TAB_WIDTH) / 2);
|
||||||
int highlightBottom = Math.max(baseHeight - 1, highlightTop + 1);
|
int activeY = baseHeight - NORMAL_ACTIVE_TAB_HEIGHT + 3;
|
||||||
g.fill(3, highlightTop, baseWidth - 3, highlightBottom, 0xFFD94A4A);
|
g.blit(
|
||||||
|
NORMAL_ACTIVE_TAB_TEXTURE,
|
||||||
|
activeX,
|
||||||
|
activeY,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
NORMAL_ACTIVE_TAB_WIDTH,
|
||||||
|
NORMAL_ACTIVE_TAB_HEIGHT,
|
||||||
|
NORMAL_ACTIVE_TAB_WIDTH,
|
||||||
|
NORMAL_ACTIVE_TAB_HEIGHT
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,6 +1114,10 @@ public final class BagTabOverlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isFlippedHorizontalShell(RenderedTab tab) {
|
||||||
|
return !tab.isVertical() && Math.round(tab.rotationDegrees()) % 360 != 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static void renderCompactLabel(GuiGraphics g, RenderedTab tab) {
|
private static void renderCompactLabel(GuiGraphics g, RenderedTab tab) {
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
Minecraft minecraft = Minecraft.getInstance();
|
||||||
int maxTextWidth = Math.max(8, tab.width() - 4);
|
int maxTextWidth = Math.max(8, tab.width() - 4);
|
||||||
@@ -1050,6 +1163,9 @@ public final class BagTabOverlay {
|
|||||||
private static List<Component> getTooltipLines(RenderedTab tab, List<BagEntry> tabs) {
|
private static List<Component> getTooltipLines(RenderedTab tab, List<BagEntry> tabs) {
|
||||||
List<Component> lines = new ArrayList<>();
|
List<Component> lines = new ArrayList<>();
|
||||||
lines.add(tab.entry().stack().getHoverName());
|
lines.add(tab.entry().stack().getHoverName());
|
||||||
|
if (tab.entry().handler() == null) {
|
||||||
|
lines.add(Component.translatable("bagtabs.tooltip.hotbar_fallback").withStyle(style -> style.withItalic(true)));
|
||||||
|
}
|
||||||
lines.add(Component.literal("Left-click: open"));
|
lines.add(Component.literal("Left-click: open"));
|
||||||
if (tab.pinned()) {
|
if (tab.pinned()) {
|
||||||
if (!DockConfigManager.isInteractionsLocked()) {
|
if (!DockConfigManager.isInteractionsLocked()) {
|
||||||
@@ -1475,6 +1591,29 @@ public final class BagTabOverlay {
|
|||||||
g.pose().popPose();
|
g.pose().popPose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void withTabLocalSpace(GuiGraphics g, RenderedTab tab, Runnable draw) {
|
||||||
|
g.pose().pushPose();
|
||||||
|
pushTabTransform(g, tab);
|
||||||
|
draw.run();
|
||||||
|
g.pose().popPose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void blitTabLocalTexture(GuiGraphics g, ResourceLocation texture, int u, int v, int regionWidth, int regionHeight) {
|
||||||
|
g.blit(texture, 0, 0, u, v, regionWidth, regionHeight, regionWidth * 2, regionHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void withLocalElementTransform(GuiGraphics g, int x, int y, int width, int height, LocalElementTransform transform, Runnable draw) {
|
||||||
|
g.pose().pushPose();
|
||||||
|
g.pose().translate(x + transform.translateX() + (width / 2.0F), y + transform.translateY() + (height / 2.0F), 0.0F);
|
||||||
|
if (transform.rotationDegrees() != 0.0F) {
|
||||||
|
g.pose().mulPose(Axis.ZP.rotationDegrees(transform.rotationDegrees()));
|
||||||
|
}
|
||||||
|
g.pose().scale(transform.flipHorizontal() ? -1.0F : 1.0F, transform.flipVertical() ? -1.0F : 1.0F, 1.0F);
|
||||||
|
g.pose().translate(-(width / 2.0F), -(height / 2.0F), 0.0F);
|
||||||
|
draw.run();
|
||||||
|
g.pose().popPose();
|
||||||
|
}
|
||||||
|
|
||||||
private static void pushTabTransform(GuiGraphics g, RenderedTab tab) {
|
private static void pushTabTransform(GuiGraphics g, RenderedTab tab) {
|
||||||
g.pose().translate(tab.x() + (tab.width() / 2.0F), tab.y() + (tab.height() / 2.0F), 0.0F);
|
g.pose().translate(tab.x() + (tab.width() / 2.0F), tab.y() + (tab.height() / 2.0F), 0.0F);
|
||||||
g.pose().mulPose(Axis.ZP.rotationDegrees(tab.rotationDegrees()));
|
g.pose().mulPose(Axis.ZP.rotationDegrees(tab.rotationDegrees()));
|
||||||
@@ -1618,6 +1757,28 @@ public final class BagTabOverlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record LocalElementTransform(int translateX, int translateY, boolean flipHorizontal, boolean flipVertical, float rotationDegrees) {
|
||||||
|
private static LocalElementTransform identity() {
|
||||||
|
return new LocalElementTransform(0, 0, false, false, 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalElementTransform translate(int x, int y) {
|
||||||
|
return new LocalElementTransform(this.translateX + x, this.translateY + y, this.flipHorizontal, this.flipVertical, this.rotationDegrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalElementTransform mirrorHorizontal() {
|
||||||
|
return new LocalElementTransform(this.translateX, this.translateY, !this.flipHorizontal, this.flipVertical, this.rotationDegrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalElementTransform mirrorVertical() {
|
||||||
|
return new LocalElementTransform(this.translateX, this.translateY, this.flipHorizontal, !this.flipVertical, this.rotationDegrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalElementTransform rotate(float degrees) {
|
||||||
|
return new LocalElementTransform(this.translateX, this.translateY, this.flipHorizontal, this.flipVertical, this.rotationDegrees + degrees);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record PendingClick(int slot, double grabOffsetX, double grabOffsetY, boolean pinned) {
|
private record PendingClick(int slot, double grabOffsetX, double grabOffsetY, boolean pinned) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.trunksbomb.bagtabs.item;
|
|||||||
import com.trunksbomb.bagtabs.BagTabs;
|
import com.trunksbomb.bagtabs.BagTabs;
|
||||||
import com.trunksbomb.bagtabs.bag.BagContainer;
|
import com.trunksbomb.bagtabs.bag.BagContainer;
|
||||||
import com.trunksbomb.bagtabs.bag.BagIdentityData;
|
import com.trunksbomb.bagtabs.bag.BagIdentityData;
|
||||||
|
import com.trunksbomb.bagtabs.bag.BagLocation;
|
||||||
|
import com.trunksbomb.bagtabs.bag.InventoryBagLocation;
|
||||||
import com.trunksbomb.bagtabs.bag.InventoryBag;
|
import com.trunksbomb.bagtabs.bag.InventoryBag;
|
||||||
import com.trunksbomb.bagtabs.menu.BagMenu;
|
import com.trunksbomb.bagtabs.menu.BagMenu;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -61,7 +63,11 @@ public class BagItem extends Item implements InventoryBag {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void openFromInventory(ServerPlayer player, int slot) {
|
public void openFromInventory(ServerPlayer player, int slot) {
|
||||||
ItemStack stack = player.getInventory().getItem(slot);
|
this.openFromLocation(player, new InventoryBagLocation(slot), slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openFromLocation(ServerPlayer player, BagLocation location, int slot) {
|
||||||
|
ItemStack stack = location.getStack(player);
|
||||||
if (!(stack.getItem() instanceof BagItem)) {
|
if (!(stack.getItem() instanceof BagItem)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -73,7 +79,7 @@ public class BagItem extends Item implements InventoryBag {
|
|||||||
new SimpleMenuProvider((containerId, playerInventory, ignoredPlayer) -> new BagMenu(
|
new SimpleMenuProvider((containerId, playerInventory, ignoredPlayer) -> new BagMenu(
|
||||||
containerId,
|
containerId,
|
||||||
playerInventory,
|
playerInventory,
|
||||||
new BagContainer(player, slot),
|
new BagContainer(player, location),
|
||||||
slot
|
slot
|
||||||
), title),
|
), title),
|
||||||
buf -> buf.writeVarInt(slot)
|
buf -> buf.writeVarInt(slot)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.trunksbomb.bagtabs.menu;
|
package com.trunksbomb.bagtabs.menu;
|
||||||
|
|
||||||
import com.trunksbomb.bagtabs.BagTabs;
|
import com.trunksbomb.bagtabs.BagTabs;
|
||||||
|
import com.trunksbomb.bagtabs.bag.BagAccess;
|
||||||
import com.trunksbomb.bagtabs.bag.BagContainer;
|
import com.trunksbomb.bagtabs.bag.BagContainer;
|
||||||
|
import com.trunksbomb.bagtabs.bag.BagEntry;
|
||||||
|
import com.trunksbomb.bagtabs.bag.InventoryBagLocation;
|
||||||
import com.trunksbomb.bagtabs.bag.InventoryBag;
|
import com.trunksbomb.bagtabs.bag.InventoryBag;
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
import net.minecraft.world.Container;
|
import net.minecraft.world.Container;
|
||||||
@@ -23,7 +26,13 @@ public class BagMenu extends AbstractContainerMenu {
|
|||||||
|
|
||||||
public static BagMenu fromNetwork(int containerId, Inventory playerInventory, RegistryFriendlyByteBuf extraData) {
|
public static BagMenu fromNetwork(int containerId, Inventory playerInventory, RegistryFriendlyByteBuf extraData) {
|
||||||
int slot = extraData.readVarInt();
|
int slot = extraData.readVarInt();
|
||||||
return new BagMenu(containerId, playerInventory, new BagContainer(playerInventory.player, slot), slot);
|
BagEntry bag = BagAccess.findBag(playerInventory.player, slot);
|
||||||
|
return new BagMenu(
|
||||||
|
containerId,
|
||||||
|
playerInventory,
|
||||||
|
new BagContainer(playerInventory.player, bag != null ? bag.location() : new InventoryBagLocation(slot)),
|
||||||
|
slot
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BagMenu(int containerId, Inventory playerInventory, Container container, int bagSlot) {
|
public BagMenu(int containerId, Inventory playerInventory, Container container, int bagSlot) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.trunksbomb.bagtabs.network;
|
package com.trunksbomb.bagtabs.network;
|
||||||
|
|
||||||
import com.trunksbomb.bagtabs.BagTabs;
|
import com.trunksbomb.bagtabs.BagTabs;
|
||||||
|
import com.trunksbomb.bagtabs.bag.BagAccess;
|
||||||
|
import com.trunksbomb.bagtabs.bag.BagEntry;
|
||||||
import com.trunksbomb.bagtabs.bag.BagCompat;
|
import com.trunksbomb.bagtabs.bag.BagCompat;
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
import net.minecraft.network.codec.ByteBufCodecs;
|
import net.minecraft.network.codec.ByteBufCodecs;
|
||||||
@@ -28,11 +30,12 @@ public record InsertIntoBagPayload(int slot) implements CustomPacketPayload {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.slot() < 0 || payload.slot() >= serverPlayer.getInventory().getContainerSize()) {
|
BagEntry entry = BagAccess.findBag(serverPlayer, payload.slot());
|
||||||
|
if (entry == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack bagStack = serverPlayer.getInventory().getItem(payload.slot());
|
ItemStack bagStack = entry.location().getStack(serverPlayer);
|
||||||
ItemStack carriedStack = serverPlayer.containerMenu.getCarried();
|
ItemStack carriedStack = serverPlayer.containerMenu.getCarried();
|
||||||
if (carriedStack.isEmpty() || bagStack.isEmpty()) {
|
if (carriedStack.isEmpty() || bagStack.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
@@ -42,8 +45,8 @@ public record InsertIntoBagPayload(int slot) implements CustomPacketPayload {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BagCompat.BagHandler handler = BagCompat.findHandler(bagStack);
|
BagCompat.BagHandler handler = entry.handler();
|
||||||
if (handler != null && handler.insertFromCarried(serverPlayer, payload.slot(), bagStack, carriedStack)) {
|
if (handler != null && handler.insertFromCarried(serverPlayer, payload.slot(), entry.location(), bagStack, carriedStack)) {
|
||||||
serverPlayer.containerMenu.broadcastChanges();
|
serverPlayer.containerMenu.broadcastChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.trunksbomb.bagtabs.network;
|
package com.trunksbomb.bagtabs.network;
|
||||||
|
|
||||||
import com.trunksbomb.bagtabs.BagTabs;
|
import com.trunksbomb.bagtabs.BagTabs;
|
||||||
|
import com.trunksbomb.bagtabs.bag.BagAccess;
|
||||||
|
import com.trunksbomb.bagtabs.bag.BagEntry;
|
||||||
import com.trunksbomb.bagtabs.bag.BagCompat;
|
import com.trunksbomb.bagtabs.bag.BagCompat;
|
||||||
import net.minecraft.core.component.DataComponents;
|
import net.minecraft.core.component.DataComponents;
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
@@ -43,41 +45,40 @@ public record OpenBagPayload(int slot) implements CustomPacketPayload {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.slot() < 0 || payload.slot() >= serverPlayer.getInventory().getContainerSize()) {
|
BagEntry entry = BagAccess.findBag(serverPlayer, payload.slot());
|
||||||
|
if (entry == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack stack = serverPlayer.getInventory().getItem(payload.slot());
|
ItemStack stack = entry.location().getStack(serverPlayer);
|
||||||
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
|
BagCompat.BagHandler handler = entry.handler();
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
handler.open(serverPlayer, payload.slot(), stack);
|
handler.open(serverPlayer, payload.slot(), entry.location(), stack);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BagCompat.canShowInTabs(stack)) {
|
if (BagCompat.canShowInTabs(stack)) {
|
||||||
if (!openViaHotbarFallback(serverPlayer, payload.slot())) {
|
if (!openViaHotbarFallback(serverPlayer, entry)) {
|
||||||
serverPlayer.sendSystemMessage(BagTabs.translation("generic_open_failed"));
|
serverPlayer.sendSystemMessage(BagTabs.translation("generic_open_failed"));
|
||||||
BagTabs.LOGGER.debug("Failed to open bag fallback for item {} in slot {}", stack, payload.slot());
|
BagTabs.LOGGER.debug("Failed to open bag fallback for item {} in slot {}", stack, payload.slot());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean openViaHotbarFallback(ServerPlayer player, int sourceSlot) {
|
private static boolean openViaHotbarFallback(ServerPlayer player, BagEntry entry) {
|
||||||
Inventory inventory = player.getInventory();
|
Inventory inventory = player.getInventory();
|
||||||
ItemStack sourceStack = inventory.getItem(sourceSlot);
|
ItemStack sourceStack = entry.location().getStack(player);
|
||||||
if (sourceStack.isEmpty()) {
|
if (sourceStack.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int targetHotbarSlot = chooseHotbarSlot(inventory, sourceSlot);
|
int targetHotbarSlot = chooseHotbarSlot(inventory, entry.slot());
|
||||||
if (targetHotbarSlot < 0) {
|
if (targetHotbarSlot < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceSlot != targetHotbarSlot) {
|
if (!entry.location().moveToHotbar(player, targetHotbarSlot)) {
|
||||||
ItemStack hotbarStack = inventory.getItem(targetHotbarSlot).copy();
|
return false;
|
||||||
inventory.setItem(targetHotbarSlot, sourceStack.copy());
|
|
||||||
inventory.setItem(sourceSlot, hotbarStack);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory.selected = targetHotbarSlot;
|
inventory.selected = targetHotbarSlot;
|
||||||
|
|||||||
@@ -37,8 +37,11 @@ public record QueryInsertTargetsPayload(int requestId) implements CustomPacketPa
|
|||||||
|
|
||||||
if (!carriedStack.isEmpty()) {
|
if (!carriedStack.isEmpty()) {
|
||||||
for (BagEntry bag : BagAccess.findBags(serverPlayer)) {
|
for (BagEntry bag : BagAccess.findBags(serverPlayer)) {
|
||||||
ItemStack bagStack = serverPlayer.getInventory().getItem(bag.slot());
|
if (bag.handler() == null) {
|
||||||
if (bag.handler().canInsertFromCarried(serverPlayer, bag.slot(), bagStack, carriedStack)) {
|
continue;
|
||||||
|
}
|
||||||
|
ItemStack bagStack = bag.location().getStack(serverPlayer);
|
||||||
|
if (bag.handler().canInsertFromCarried(serverPlayer, bag.slot(), bag.location(), bagStack, carriedStack)) {
|
||||||
insertableSlots.add(bag.slot());
|
insertableSlots.add(bag.slot());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"container.bagtabs.bag": "Bag",
|
"container.bagtabs.bag": "Bag",
|
||||||
"container.bagtabs.bag_namer": "Bag Namer",
|
"container.bagtabs.bag_namer": "Bag Namer",
|
||||||
"bagtabs.tooltip.click_to_open": "Open from your inventory tabs",
|
"bagtabs.tooltip.click_to_open": "Open from your inventory tabs",
|
||||||
|
"bagtabs.tooltip.hotbar_fallback": "This container is not fully supported",
|
||||||
"bagtabs.gui.bag_namer.name": "New Name",
|
"bagtabs.gui.bag_namer.name": "New Name",
|
||||||
"bagtabs.gui.bag_namer.placeholder": "Leave blank to clear",
|
"bagtabs.gui.bag_namer.placeholder": "Leave blank to clear",
|
||||||
"bagtabs.gui.bag_namer.rename": "Rename",
|
"bagtabs.gui.bag_namer.rename": "Rename",
|
||||||
|
|||||||
BIN
src/main/resources/assets/bagtabs/textures/gui/bookmark.png
Normal file
|
After Width: | Height: | Size: 399 B |
|
After Width: | Height: | Size: 399 B |
|
After Width: | Height: | Size: 362 B |
6
src/main/resources/data/curios/tags/item/back.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"replace": false,
|
||||||
|
"values": [
|
||||||
|
"bagtabs:bag"
|
||||||
|
]
|
||||||
|
}
|
||||||
6
src/main/resources/data/curios/tags/item/belt.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"replace": false,
|
||||||
|
"values": [
|
||||||
|
"bagtabs:bag"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.trunksbomb.bagtabs.api.examples;
|
||||||
|
|
||||||
|
import com.trunksbomb.bagtabs.api.BagTabsApi;
|
||||||
|
import com.trunksbomb.bagtabs.api.BagTabsIdentity;
|
||||||
|
import com.trunksbomb.bagtabs.api.BagTabsLocation;
|
||||||
|
import com.trunksbomb.bagtabs.api.BagTabsProvider;
|
||||||
|
import java.util.UUID;
|
||||||
|
import net.minecraft.core.component.DataComponents;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.SimpleMenuProvider;
|
||||||
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||||
|
import net.minecraft.world.item.Item;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.component.ItemContainerContents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example-only provider registration that other mod authors can copy and adapt.
|
||||||
|
*
|
||||||
|
* <p>This class is intentionally placed under src/test so it does not ship in the final mod jar.
|
||||||
|
*/
|
||||||
|
public final class ExampleBagTabsCompat {
|
||||||
|
private ExampleBagTabsCompat() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
BagTabsApi.registerProvider(new ExampleProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ExampleProvider implements BagTabsProvider {
|
||||||
|
@Override
|
||||||
|
public boolean supports(ItemStack stack) {
|
||||||
|
return stack.getItem() instanceof ExampleBagItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BagTabsIdentity getIdentity(ItemStack stack) {
|
||||||
|
return new BagTabsIdentity("examplemod:bag:" + ExampleBagItem.getOrCreateBagId(stack), "uuid", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void open(ServerPlayer player, int slot, BagTabsLocation location, ItemStack stack) {
|
||||||
|
ExampleBagItem.openFromLocation(player, location, slot, stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActiveMenu(AbstractContainerMenu menu, Player player, int slot, BagTabsLocation location, ItemStack stack) {
|
||||||
|
return menu instanceof ExampleBagMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getFullness(ItemStack stack) {
|
||||||
|
ItemContainerContents contents = stack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY);
|
||||||
|
return contents.equals(ItemContainerContents.EMPTY) ? 0.0F : 1.0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal fake bag item used only to demonstrate the API shape.
|
||||||
|
*/
|
||||||
|
private static class ExampleBagItem extends Item {
|
||||||
|
private static final String BAG_ID_KEY = "examplemod-bag-id";
|
||||||
|
|
||||||
|
private ExampleBagItem() {
|
||||||
|
super(new Item.Properties().stacksTo(1).component(DataComponents.CONTAINER, ItemContainerContents.EMPTY));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getOrCreateBagId(ItemStack stack) {
|
||||||
|
String existing = stack.getOrDefault(DataComponents.CUSTOM_NAME, Component.empty()).getString();
|
||||||
|
if (!existing.isBlank()) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
String generated = BAG_ID_KEY + "-" + UUID.randomUUID();
|
||||||
|
stack.set(DataComponents.CUSTOM_NAME, Component.literal(generated));
|
||||||
|
return generated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openFromLocation(ServerPlayer player, BagTabsLocation location, int slot) {
|
||||||
|
player.openMenu(new SimpleMenuProvider(
|
||||||
|
(containerId, inventory, ignored) -> new ExampleBagMenu(containerId, inventory),
|
||||||
|
Component.literal("Example Bag")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal fake menu used only to demonstrate active-menu reporting.
|
||||||
|
*/
|
||||||
|
private static final class ExampleBagMenu extends AbstractContainerMenu {
|
||||||
|
private ExampleBagMenu(int containerId, Inventory inventory) {
|
||||||
|
super(null, containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack quickMoveStack(Player player, int index) {
|
||||||
|
return ItemStack.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean stillValid(Player player) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||