Compare commits
3 Commits
a90227ff02
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c5c35871d | ||
|
|
5d283909db | ||
|
|
8189359e91 |
43
.gitea/workflows/release.yml
Normal file
43
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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: Make Gradle wrapper executable
|
||||||
|
run: chmod +x ./gradlew
|
||||||
|
|
||||||
|
- name: Build release jar
|
||||||
|
run: ./gradlew --no-daemon clean build
|
||||||
|
|
||||||
|
- name: Publish release artifacts
|
||||||
|
run: python3 ./scripts/publish_release.py
|
||||||
109
README.md
109
README.md
@@ -1,25 +1,96 @@
|
|||||||
|
# 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.
|
- 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/
|
- 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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -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
BIN
project_picture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 798 B |
BIN
project_picture_full.png
Normal file
BIN
project_picture_full.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
397
scripts/publish_release.py
Normal file
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()
|
||||||
@@ -1050,6 +1050,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()) {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user