Each pseudo-page below follows the same loader selection.
PyFish Documentation
PyFish embeds GraalPy into Minecraft so Python code can run at runtime on NeoForge or Fabric. The loader switch persists across the whole documentation, but all of the content now stays in one HTML file to avoid drift between separate pages.
NeoForge currently exposes the broadest documented event surface.
Runtime Python for Minecraft
PyFish is built for people who want Minecraft behavior to be scriptable without rebuilding a Java mod for every small gameplay change. It can load Python from the jar, from an external instance folder, from packaged .pyz mods, and from organized logical Python mods inside pyfish/.
One mc surface
World manipulation, messaging, commands, spawning, particles, and more use the same Python method names on both loaders.
Client-only, server-only, or both
PyFish is optional by default on each side. Server owners can still turn on a strict client requirement when they need it.
Single HTML documentation
This documentation behaves like multiple pages, but everything lives in one HTML file so the API, loader differences, and event matrix stay aligned.
Documentation map
Each card below jumps to a dedicated view inside this file. The layout keeps the feeling of separate pages while avoiding duplicated content.
Installation
PyFish is installed like a normal mod jar, but the exact jar name depends on the selected loader. This view also covers deployment modes and the server policy that can require the client mod.
Keep Minecraft on 1.21.1 and Java on 21.
How to install PyFish
Download the PyFish jar matching both your loader and Minecraft version, then place it in the normal mods/ folder. Do not mix a Fabric jar with NeoForge or install a jar made for another Minecraft version.
- Install Fabric Loader 0.16.9 for Minecraft 1.21.1.
- Install Fabric API 0.116.12+1.21.1 for Minecraft 1.21.1.
- Copy pyfish-fabric-1.21.1-1.2.0.jar into mods/.
- Install NeoForge 21.1.215 for Minecraft 1.21.1.
- Copy pyfish-neoforge-1.21.1-1.2.0.jar into mods/.
- Start the game once so the standard PyFish config folders are created.
- Minecraft version: 1.21.1
- Java version: 21
- Python runtime: bundled GraalPy
Client-only, server-only, or both
Most common scripting setup
Best for gameplay automation, event-driven logic, and modpack rules running on a dedicated or integrated server. By default, clients can join without installing PyFish.
Optional local runtime
Useful when you want the mod on the client ahead of time or when a server may decide to require it. Client-only does not expose server-side gameplay control by itself.
Full setup
Use both sides when you want server logic and the option to make the client mod mandatory from the server configuration.
Requiring the client mod
PyFish is optional by default, but server owners can flip one config setting to require the client mod. This policy is shared across Fabric and NeoForge.
requireClientPyFish=false clientHandshakeTimeoutTicks=60 missingClientMessage=This server requires PyFish on the client.
config/pyfish/server.properties
The server waits for a PyFish client hello. If the client never answers before the timeout, that player is disconnected with the configured message.
You can keep the normal install experience optional for most players while still giving server or modpack authors a clean opt-in enforcement model.
External Scripts
PyFish can load Python from the jar and from the external pyfish/ folder. This view focuses on the external layout used by servers, local instances, and modpacks.
How external scripts are organized
PyFish scans scripts from the root pyfish/ folder. Packaged Python-only mods use the normal Minecraft mods/ folder, right next to PyFish and the other mod JARs.
.minecraft/
|-- mods/
| |-- pyfish-neoforge-1.21.11-1.2.0.jar
| `-- packaged_mod-1.0.0.pyz
|-- config/
`-- pyfish/
|-- scripts/
| `-- global_fix.py
`-- my_mod/
|-- requirements.txt
|-- assets/
| `-- my_mod/
| `-- textures/
| `-- item/
| `-- ruby.png
|-- data/
| `-- my_mod/
| `-- recipes/
| `-- ruby.json
`-- scripts/
|-- init.py
`-- gameplay.py
pyfish/scripts/
Files in this folder are loaded without a logical mod id. They are useful for quick global tweaks or debugging scripts.
pyfish/<mod_id>/scripts/
Each folder acts like a small Python-powered content pack. Grouping scripts this way keeps pack-specific logic easier to maintain.
assets/ and data/
Logical mods can also bundle normal Minecraft resources under pyfish/<mod_id>/assets/<namespace>/... and pyfish/<mod_id>/data/<namespace>/....
requirements.txt
Pure Python dependencies can live next to a logical mod. PyFish keeps dependency installation scoped by mod id instead of flattening everything together.
mods/*.pyz
A Python zipapp packages the manifest, entrypoint, optional dependencies, and internal modules into one distributable file. The same archive works on Fabric and NeoForge.
Jar resources and external files can coexist
Scripts inside a jar are useful when PyFish is shipped as a reusable base or when a Python-powered mod wants self-contained packaged logic.
Files ending in .pyz inside the normal mods/ folder are validated before execution. Their manifest id becomes the content namespace, bundled assets/ and data/ are exposed automatically, and dependencies stay isolated under it.
The instance folder is ideal for modpack authors because it keeps iteration fast and avoids rebuilding jars for small rule changes.
Fabric and NeoForge use the same script folder layout. Loader differences mainly affect event availability, not script discovery.
Getting Started
Start with one tiny script that proves the runtime, event registration, callback execution, and shared Minecraft API are all alive.
First runtime check
from pyfish import mc, log_print, ServerEvent, PlayerEvent
def on_server_started(event: ServerEvent):
log_print("PyFish runtime ready")
def on_player_join(event: PlayerEvent):
player = event.getPlayer() or event.getEntity()
mc.send_message(player, "PyFish is active on this server.")
print("Hello World!")
mc.on("on_server_started", on_server_started)
mc.on("on_player_join", on_player_join)
What success looks like
You should see the script load in the log and the runtime should register the callbacks without an unsupported-event warning.
When the world or dedicated server finishes starting, the server-started callback should log its message cleanly.
Joining the game should trigger the chat message without loader-specific attribute errors, which is a strong sign that the callback objects and unwrapping helpers are aligned.
Callback Objects
PyFish wraps Fabric events into shared Python-facing objects whenever possible. NeoForge still exposes the native event object for many specialized hooks, so this view focuses on the shared wrapper surface that scripts can rely on. These same class names are safe to import and use in Python type annotations under GraalPy.
Common wrapper objects
Wraps a server player and can be passed directly into methods like mc.send_message(...) or mc.teleport(...).
Wraps a server entity. Useful in generic entity callbacks and interaction callbacks.
Wraps a block position object and is accepted by the shared unwrapping helpers inside the Python bridge.
Annotations are runtime-safe
You can annotate callbacks exactly the same way you want the IDE to see them. GraalPy does not require stripping those annotations before the script runs inside the mod.
from pyfish import mc, PlayerEvent, PlayerChatEvent
def on_player_join(event: PlayerEvent):
player = event.getPlayer() or event.getEntity()
if player is not None:
mc.send_message(player, "Join event typed correctly")
def on_player_chat(event: PlayerChatEvent):
player = event.getPlayer()
if player is not None:
mc.send_message(player, f"You said: {event.getMessageString()}")
mc.on("on_player_join", on_player_join)
mc.on("on_player_chat", on_player_chat)
What Python callbacks can see on Fabric
PlayerEvent, PlayerCloneEvent, ServerEvent, and LevelEvent cover join, leave, respawn, clone, server lifecycle, and world lifecycle hooks.
BlockBreakEvent, PlayerInteractBlockEvent, and PlayerInteractEntityEvent expose player, level, position, hand, and target information through loader-agnostic helpers.
EntitySpawnEvent, EntityDimensionChangeEvent, LivingDamageEvent, and LivingDeathEvent now cover spawn, dimension change, hurt, damage, and death in the Fabric bridge.
When NeoForge still exposes the raw event
Loader-specific aliases such as on_item_crafted, on_explosion, or on_command_executed still forward the native NeoForge event object. That is intentional: Fabric does not provide a clean shared equivalent for all of those hooks yet.
Loader Differences
PyFish tries to keep the Python-facing API stable across both loaders, but the event bridge is not perfectly symmetrical. This view tracks what is genuinely shared and what remains loader-specific.
Widest current bridge: broader entity, item, block, and lifecycle coverage.
Shared bridge: lifecycle, player, world, chat, entity lifecycle, and living damage callbacks.
Widest current bridge: broader entity, item, block, and lifecycle coverage.
- The same pyfish/ folder layout
- The same Python mc method names
- The same optional-by-default client/server model
- The same server config for requiring the client mod
- Server and world lifecycle hooks
- Player join, leave, respawn, clone, chat, and tick
- Block break and player interaction hooks
- Entity spawn and dimension changes
- Living hurt, damage, and death observation
- Specialized block hooks like crop growth or note block play
- Specialized player hooks like sleep, wake-up, and XP changes
- Item crafting, smelting, pickup, and toss callbacks
- Specialized entity hooks such as jump, heal, fall, and tame
- Explosion and command pipeline callbacks
Dynamic full-class registration is NeoForge-only
mc.on("full.java.ClassName", callback) is meaningful on NeoForge because the bridge can resolve arbitrary event classes on that loader. Fabric uses the documented aliases only.
Dynamic Content API
Items, blocks, recipes, tags, textures, and tabs now live directly in pyfish. Every API card below can open its own dedicated in-file page with a fuller explanation, tested usage notes, and a more complete example.
Register content directly from Python
PyFish exposes its content workflow directly from the base pyfish module. (from pyfish import items, blocks, recipes, tags, textures, tabs).
Registers a simple item. If the id has no namespace, PyFish resolves it from the current script context.
Open full pageRegisters a food item with nutrition and saturation values.
Open full pageRegisters a tool or weapon such as a sword, pickaxe, axe, hoe, or shovel.
Open full pageRegisters a block and generates matching blockstate, model, and item-model resources.
Open full pageGenerates crafting recipes directly from Python without a separate data pack step.
Open full pageAdds generated item or block entries to tags, again with automatic namespace resolution.
Open full pageRegisters generated texture data at runtime for items or blocks.
Open full pageCreates a creative tab from Python. Tabs, like items and blocks, inherit the active script namespace by default.
Open full pageHow ids are resolved
Scripts in pyfish/scripts/ register content under the pyfish namespace by default.
Scripts in pyfish/<mod_id>/scripts/ register content under <mod_id>.
Scripts bundled inside a proper mod jar register content under that Java mod id, so a PyFish-powered mod stays inside its own namespace automatically.
What the registries actually accept
- texture picks the generated model texture reference.
- model overrides the item model parent.
- stacksTo, fireResistant, and durability are supported on items.
- alwaysEdible is supported on foods.
- tab attaches the entry to a generated or existing creative tab.
- events attaches Python callbacks directly to the dynamic item.
- Supported tool types: sword, pickaxe, shovel, axe, hoe, and multitool.
- damage and attackSpeed control combat values.
- durability, speed, damageBonus, enchantmentValue, and repairItem shape the generated tier.
- PyFish also adds the matching vanilla tool tags automatically.
- texture can be a single texture id or a map such as top/bottom/side, end/side, or a full north/south/east/west/up/down set.
- destroyTime, explosionResistance, and requiresCorrectToolForDrops are supported.
- Registering a block also generates and registers the matching block item automatically.
- textures.register_texture(...) takes raw bytes, while textures.register_image(...) takes a Java BufferedImage.
- Recipes are generated directly at runtime and support namespace resolution for ids and ingredients.
- Tag ingredients can use the normal #namespace:path syntax.
- tags.unify(...) currently records intent in the log but does not rewrite the registry automatically.
- Creative tabs currently use displayName and icon. Use displayName, not title.
Python handlers on items and blocks
PyFish supports dynamic content callbacks through the events property. Handlers receive raw Java-side objects for the content interaction itself.
- use runs when the item is used in the air.
- use_on runs when the item is used on a block.
- eat runs when the food finishes being eaten.
- use runs when the block is interacted with.
- The content callback layer is shared across Fabric and NeoForge because it lives above the loader-specific registries.
def on_wand_use(level, player, hand):
mc.send_message(player, "Dynamic item callback OK")
items.register_item("signal_wand", {
"texture": "signal_wand",
"stacksTo": 1,
"events": {
"use": on_wand_use
}
})
World manipulation is documented separately now
Content registration and live world manipulation used to share one long page, which made both areas harder to scan. The dedicated World API page now covers block and time helpers with their own larger cards and detailed pseudo-pages.
Open the World API page
World API
These helpers read or change the live world state. They are the functions you usually call from callbacks once you already have a player, world, server, or position handle.
Blocks and time
The world argument can be a real Java ServerLevel or a wrapper returned by a PyFish callback. The bridge unwraps both forms automatically.
Returns the registry id of the block at the given coordinates.
Open full pagePlaces or replaces a block. This is one of the fastest visible checks for the shared runtime bridge.
Open full pageBreaks a block and lets vanilla drops and normal world logic run.
Open full pageReads the current day-night tick value from the world.
Open full pageSets the world time to a specific tick value such as 0 or 13000.
Open full pageUse callback wrappers directly
from pyfish import mc, PlayerEvent
def on_player_join(event: PlayerEvent):
player = event.getPlayer() or event.getEntity()
world = event.getLevel() or player.serverLevel()
x = player.getBlockX() + 1
y = player.getBlockY()
z = player.getBlockZ()
mc.set_block(world, x, y, z, "minecraft:gold_block")
mc.send_message(player, f"Placed {mc.get_block_id(world, x, y, z)} at {x} {y} {z}")
mc.on("on_player_join", on_player_join)
Players and Entities API
These helpers cover messaging, movement, health, inventory, entity spawning, and simple entity queries.
Sends a system message to one player. Player handles from callbacks work directly.
Open full pageBroadcasts a message to players in a world.
Open full pageGives an item stack to a player.
Open full pageReturns the players in the selected world.
Open full pageTeleports a player to the given coordinates.
Open full pageSets current health.
Open full pageSets hunger from 0 to 20.
Open full pageSets the experience level.
Open full pageApplies a potion effect such as minecraft:speed.
Open full pageSpawns an entity in the world.
Open full pageRemoves an entity from the world.
Open full pageReturns every entity inside a radius around a point.
Open full page
Utils and Environment API
These methods cover commands, explosions, lightning, sounds, particles, and event registration behavior.
Executes a server command without a leading slash.
Open full pageCreates an explosion. The mode can be block, none, mob, or tnt.
Open full pageSpawns lightning at the target coordinates.
Open full pagePlays a sound event in the world.
Open full pageSpawns particles with count, spread, and speed parameters.
Open full pageRegisters a callback for a documented alias on both loaders. On NeoForge, you can also pass a full Java event class name. On Fabric, only the documented aliases are supported.
Open full page
Shared runtime helper
Part of the PyFish API reference.
Use this from callbacks or command-driven scripts.
What it does
Parameters and return value
| Argument | Meaning |
|---|
No return value.
Complete example
Things worth knowing
Keep exploring
Event System
Register callbacks with mc.on("alias", callback). Use the switch to inspect the exact supported aliases and backend hooks for Fabric or NeoForge.
The Python callback class column shows the shared PyFish wrapper class when one exists, so you can annotate callbacks such as def on_player_chat(event: PlayerChatEvent):. For specialized NeoForge-only aliases, the same column names the native event class that is forwarded instead. Names like BlockEvent.EntityPlaceEvent are nested NeoForge Java event classes.
| Alias | Status | Selected bridge | Python callback class | Side | Callback access | Notes |
|---|
IDE Support
PyFish now ships a local installable shim package so a normal Python interpreter can understand the PyFish API surface in editors. The goal is autocomplete, hover documentation, and linting without pretending that Minecraft itself is running.
Download the ZIP below, extract it, run install_editable.bat, then run python verify_install.py.
Double-click it inside the extracted ide_support folder.
Use it right after install to confirm the modules resolve in the target interpreter.
Simple zip-friendly installation flow
1. Download ide_support.zip 2. Extract the ZIP 3. Open the extracted ide_support folder 4. Double-click install_editable.bat 5. Run: python verify_install.py 6. In your IDE, select the same Python interpreter used for the install
Install from the extracted folder
python -m pip install -e . python verify_install.py
Point the IDE at the same interpreter
- Open the PyFish script folder.
- Press Ctrl+Shift+P.
- Run Python: Select Interpreter.
- Select the interpreter where ide_support was installed.
- Open project settings.
- Go to Python Interpreter.
- Select the interpreter where ide_support was installed.
- Re-open a PyFish script and confirm imports resolve.
Shared wrappers and NeoForge-native event names
- PlayerEvent, PlayerChatEvent, BlockBreakEvent, PlayerInteractBlockEvent, EntitySpawnEvent, and LivingDamageEvent are available directly from pyfish.
- These are the main classes to annotate on callbacks that are shared across Fabric and NeoForge.
- The shim also exposes nested NeoForge event names such as BlockEvent.EntityPlaceEvent, PlayerXpEvent.XpChange, ExplosionEvent.Detonate, and PlayerEvent.ItemCraftedEvent.
- That means editor annotations can mirror the exact class names shown in the Event System page.
What the shim exposes
- pyfish
- java_asyncio, java_logging, java_multiprocessing
- java_pathlib, java_sqlite3, java_subprocess
- java_uuid, java_pyperclip, java_websocket, java_requests, java_pillow
- java_requests can piggy-back on a local requests install when present.
- java_pillow can piggy-back on Pillow when present.
- java_websocket can piggy-back on websocket-client when present.
- Without those optional packages, the shim stays importable and raises a clear runtime message only when the missing feature is actually used.
Native Libraries
PyFish bundles Java-backed Python modules so common workflows still feel familiar inside Minecraft, even without CPython native extensions.
PyFish documents the Java-backed helper modules that are bundled with the base mod.
java_multiprocessing is thread-backed, not process-isolated. java_requests and java_websocket depend on network access.
java_subprocess depends on local OS permissions and tools, while java_pyperclip is mainly meaningful on a desktop client.
HTTP helpers backed by Java networking.
Official Requests docsSQLite support through JDBC, exposed with a Python-style surface.
Official sqlite3 docsAsync helpers built on Java concurrency primitives.
Official asyncio docsMultiprocessing-style helpers implemented with Java threads and executors.
Path and filesystem helpers backed by java.nio.file.
Official pathlib docsBridges Python-style logging to the Minecraft log output.
Official logging docsWebSocket client helpers for live service connections.
Official websocket-client docsUUID generation and parsing helpers.
Official uuid docsSubprocess-like helpers backed by Java process APIs.
Official subprocess docsClipboard helpers for desktop environments.
Official pyperclip docsBasic image helpers inspired by Pillow.
Official Pillow docs
Create a PyFish Mod
Start by choosing how people will install your mod. Both formats use the same Python API, but they solve different distribution needs.
Choose one format
Most Python-only projects should use .pyz. Choose JAR only when the project needs to appear as a native Fabric or NeoForge mod.
Python .pyz
One compact Python mod file shared by Fabric and NeoForge.
- No Gradle or Java project required.
- The included builder creates the file for you.
- Users place it beside PyFish in mods/.
- Best when every feature uses the PyFish API.
Fabric / NeoForge JAR
Normal loader mods containing PyFish scripts and loader metadata.
- Appears in the Fabric or NeoForge mod list.
- Other mods can declare it as a dependency.
- Produces one JAR per loader.
- Uses Gradle, but gameplay code can remain entirely Python.
The short answer
If you are unsure, choose .pyz. It is simpler, loader-neutral, and made specifically for Python-only PyFish mods. Choose JAR only when native loader metadata is useful to your project.
What both formats have in common
Users still install the normal PyFish JAR matching their loader and Minecraft version.
Your mod id becomes the namespace for unqualified item, block, recipe, tag, texture, and creative-tab ids.
Build a Python .pyz Mod
Download a small template, edit two files, run one Python command, and place the generated archive in Minecraft's normal mods/ folder.
What you need
No Gradle or Java development environment is required.
The same archive can be installed with Fabric or NeoForge.
Place it directly beside the normal PyFish JAR.
The builder creates one structured archive
A .pyz is one file when distributed, but internally it stores metadata, an entrypoint, and optional Python packages. You edit the source folder; the builder assembles this archive for you.
example_pyz_mod-1.0.0.pyz
|-- pyfish.mod.json
|-- __main__.py
|-- requirements.txt # optional
|-- assets/example_pyz_mod/ # optional bundled resources
|-- data/example_pyz_mod/ # optional bundled data
|-- example_pyz_mod/ # optional internal package
|-- __init__.py
|-- content.py
|-- events.py
Declarative mod metadata. PyFish reads and validates it before executing Python, so the mod id cannot depend on arbitrary startup code.
The standard zipapp entrypoint. This is where the mod imports PyFish, registers content, and subscribes to events.
Only needed inside an internal Python package. It is not the manifest and is not required for a small mod that keeps all code in __main__.py.
Declare identity without executing code
{
"schema_version": 1,
"id": "example_pyz_mod",
"name": "Example PyFish Mod",
"version": "1.0.0",
"description": "A Python-only Minecraft mod.",
"authors": ["Your name"],
"license": "MIT",
"homepage": ""
}
Register content and events normally
from pyfish import PlayerEvent, PyFishModMetadata, items, log_print, mc
PYFISH_MOD: PyFishModMetadata
PYFISH_MOD_ID: str
PYFISH_MOD_NAME: str
PYFISH_MOD_VERSION: str
items.register_item("python_token", {
"texture": "minecraft:item/diamond",
"stacksTo": 64,
})
def on_player_join(event: PlayerEvent) -> None:
player = event.getPlayer() or event.getEntity()
if player is None:
return
mc.send_message(player, f"{PYFISH_MOD_NAME} is active")
mc.give_item(player, f"{PYFISH_MOD_ID}:python_token", 1)
mc.on("on_player_join", on_player_join)
log_print(f"Loaded {PYFISH_MOD_NAME} {PYFISH_MOD_VERSION}")
Unqualified content ids use the validated manifest id. In this example, python_token becomes example_pyz_mod:python_token.
PYFISH_MOD contains the whole manifest. The id, name, and version also have dedicated string globals for convenient use and IDE annotations.
Four steps from template to Minecraft
The builder creates the archive. Do not rename a normal ZIP by hand: use build.py so the internal layout is always correct.
Extract the downloaded template
Open pyfish_pyz_template/. All editable project files are inside its src/ folder.
Edit metadata and Python code
Fill in src/pyfish.mod.json, then write the mod in src/__main__.py. Add packages, bundled assets/, and data/ under src/ when needed.
Run the builder
Open a terminal in the template folder and run one of these commands:
py -3 build.py # Or: python build.py
The result appears in dist/<mod_id>-<version>.pyz.
Move the result into Minecraft
Install the generated file beside PyFish. Share this generated file with users.
.minecraft/
`-- mods/
|-- pyfish-fabric-1.21.1-1.2.0.jar
`-- example_pyz_mod-1.0.0.pyz
Scale beyond one Python source file
Place it at the archive root. PyFish installs those dependencies into pyfish/libs/<mod_id>/ and reuses its existing hash-based dependency cache.
Install required PyFish archives and loader mods alongside your archive for now. Manifest-level dependency resolution is not part of the current pyfish.mod.json format yet.
Package larger projects below a folder matching the mod id, then import with from example_pyz_mod.content import register_content. The archive remains on Python's import path for later event callbacks.
Place static files in the archive with the normal Minecraft layout such as assets/<namespace>/textures/... and data/<namespace>/recipes/....
Pure-Python packages are the most portable. Packages requiring native CPython wheels may not support GraalPy and should be tested on every target environment.
Server logic or synchronized content
Event handlers, commands, automation, and server rules can run only on the server when they do not register content required by clients.
Local helpers can run only on the client when they do not change server gameplay or synchronized registries.
Install PyFish and the same mod on the server and every client when it registers items, blocks, creative tabs, textures, or other synchronized content.
Invalid archives fail clearly
- An archive missing pyfish.mod.json or __main__.py is rejected before execution.
- Duplicate or unsafe ZIP paths, unsupported schema versions, invalid ids, archives above 128 MiB uncompressed, and archives above 10,000 entries are rejected.
- If another folder, bundled script, or archive already uses the same namespace, the conflicting .pyz is rejected instead of silently overriding content.
- A .pyz contains executable Python and has the same host access as other PyFish scripts. Only install archives from sources you trust.
Build Fabric and NeoForge JAR Mods
Package Python scripts as normal loader mods. The template generates one Fabric JAR and one NeoForge JAR while keeping gameplay code in Python.
Use JAR only when loader metadata matters
The wrapper is included, so Gradle does not need to be installed separately.
One for Fabric and one for NeoForge.
The same scripts and namespace are packaged into both outputs.
What changes compared with .pyz?
A JAR has native Fabric or NeoForge metadata and appears in the loader's mod list. This is useful for dependencies and conventional mod distribution, but users need the JAR matching their loader. If none of that matters, the .pyz format is simpler.
Only one folder contains your gameplay code
Both loader JARs receive the same scripts. Fabric and NeoForge metadata are generated from the shared values in gradle.properties.
pyfish_mod_template/
|-- gradle.properties
|-- build.gradle
|-- gradlew.bat
|-- gradlew
`-- src/main/resources/
|-- fabric.mod.json
|-- META-INF/neoforge.mods.toml
|-- assets/ruby_tools/...
|-- data/ruby_tools/...
`-- pyfish/template_mod/
|-- requirements.txt
`-- scripts/main.py
Four ordered steps
Edit gradle.properties
Set mod_id, name, version, authors, license, and description. Use a unique lowercase mod id such as ruby_tools.
Rename the script namespace folder
Rename src/main/resources/pyfish/template_mod/ to src/main/resources/pyfish/ruby_tools/. The folder name must exactly match mod_id.
Write Python scripts
Put scripts in the namespace folder's scripts/ directory. Add pure-Python packages to its requirements.txt when needed, and put static resources in src/main/resources/assets/<mod_id>/... and src/main/resources/data/<namespace>/....
Build both loaders
gradlew.bat build # Linux or macOS: ./gradlew build
The command creates both files in build/libs/.
Install only the matching loader JAR
build/libs/ruby_tools-fabric-1.0.0.jar build/libs/ruby_tools-neoforge-1.0.0.jar
Install the -fabric- JAR, PyFish for Fabric, and Fabric API.
Install the -neoforge- JAR and PyFish for NeoForge.
The loader mod id becomes the content namespace
from pyfish import blocks, items
items.register_item("ruby", {...})
blocks.register_block("ruby_block", {...})
With mod_id=ruby_tools, these become ruby_tools:ruby and ruby_tools:ruby_block on both loaders.
Custom content normally belongs on both sides
Install the matching JAR and PyFish on the server and every client when the mod registers items, blocks, creative tabs, textures, or other synchronized content. Pure server logic can remain server-only.