Unified Mod Development Interface — Provides standardized capabilities for NeoForge 1.21.1 mods, including a durable block system, recipe parsing, and console log capture.
Version: 1.1.0-1.21.1 | Platform: NeoForge 1.21.1 | Java 21
Add the following to your build.gradle:
repositories {
maven { url = 'https://your-maven-repo' }
}
dependencies {
implementation 'com.cloudworks:CloudWorksAPI:1.1.0-1.21.1'
}
If you need Jade durability display for durable blocks, add the Jade API dependency:
repositories {
maven { url = "https://api.modrinth.com/maven" }
}
dependencies {
compileOnly "maven.modrinth:jade:15.10.5+neoforge"
}
CloudWorks API uses annotations to activate modules on demand. Add the corresponding annotation to your main mod class:
@Mod("your_mod_id")
@CloudworksRecipeParser // Enables the RecipeParser module
@CloudWorksConsoleSeeker // Enables ConsoleSeeker + API
public class YourMod {
public YourMod(IEventBus modEventBus) {
// ...
}
}
DurableBlock provides a complete durable block system that allows blocks to have durability values, damage resistance, and automatic recovery.
Unlike traditional solutions, this system does not rely on BlockEntity. Instead, it uses a LivingEntity subclass (DurableBlockEntity) to store durability data and receive damage. This design combines the data persistence of BlockEntity with the attackability of LivingEntity.
DurableBlock (Block)
│ Spawns entity on placement
│ Properties: maxDurability, baseResistance, type-specific resistances
▼
DurableBlockEntity (LivingEntity)
│ Server-side only entity (clientTrackingRange=0)
│ Bound to block coordinates (boundPos), validates block existence every tick
│ Durability calculation, damage handling, NBT persistence
│
├── Normal flow: durability reaches zero → breaks block → self-destructs
└── PersistentDurableBlockEntity (subclass)
└── Code game protection: intercepts instant-kill attacks → 20% durability loss
| Class | Responsibility |
|---|---|
DurableBlock |
Durable block base class, defines durability attributes and resistance parameters, manages entity spawning |
DurableBlockEntity |
Block entity replacement (LivingEntity), handles damage, durability calculation, data persistence |
PersistentDurableBlock |
Persistent durable block, extends DurableBlock, uses PersistentDurableBlockEntity |
PersistentDurableBlockEntity |
Persistent entity, intercepts instant-kill attacks, converts to durability loss |
DurableBlockDamageType |
Damage type enum (EXPLOSION / PHYSICAL / MAGIC) |
public class MyConcreteBlock extends DurableBlock {
public MyConcreteBlock() {
super(Properties.of()
.strength(3.0f)
.requiresCorrectToolForDrops(),
200, // maxBlockDurability: Maximum durability
2, // baseResistance: Flat damage reduction
0.3f, // explosionResistance: Explosion damage reduction (0.0 ~ 1.0)
0.5f, // physicalResistance: Physical damage reduction (0.0 ~ 1.0)
0.1f, // magicResistance: Magic damage reduction (0.0 ~ 1.0)
0 // recoveryRate: Auto-recovery per second (0 = none)
);
}
}
public class MyPersistentBlock extends PersistentDurableBlock {
public MyPersistentBlock() {
super(Properties.of().strength(3.0f),
200, 2, 0.3f, 0.5f, 0.1f, 0);
}
}
actualDamage = max(0, rawDamage - baseResistance) × (1 - typeResistance)
Durable block entities are registered with clientTrackingRange(0), meaning the entity exists only on the server. The client has no knowledge of the entity's existence. This provides the following benefits:
PersistentDurableBlockEntity uses a multi-layer interception mechanism to resist instant-kill attacks:
remove() Interception: Checks internalRemoval flag; external calls trigger 20% durability loss insteadsetHealth() Interception: Prevents external health zeroingsetPose() Interception: Prevents Pose.DYING statehurt() Detection: Checks instant_kill_weapon tag, cancels instant-kill weapon damage eventsdead State Correction: Checks and corrects the dead field every tickThe cloudworks_api:instant_kill_weapon item tag marks instant-kill weapons. When an entity holding a tagged item attacks, PersistentDurableBlockEntity will cancel the damage event directly.
Default contents:
{
"values": [
"avaritia:infinity_sword"
]
}
Modpack authors can add more weapons to this tag via datapack.
DurableBlock.setPlacedBy() spawns the entity at the block positiononDurabilityZero() breaks the block and removes the entityaddAdditionalSaveData / readAdditionalSaveDataEach entity stores its bound block position in boundPos. When the entity is about to disappear for any reason, it destroys the block at that position. Simultaneously, it checks every tick whether the block at that position still exists and matches the expected type; if not, it removes itself. This design prevents the bug where an entity's collision box extending into adjacent block areas would incorrectly delete other entities.
DurableBlock includes built-in Jade integration (DurableBlockJadePlugin) that displays block durability information in Jade tooltips:
Durability: current / max on the line below the block nameIServerDataProvider and synced to the client via Jade's data packetsThe entity collision box is 1.02×1.02 (slightly larger than a 1×1 block), centered at the block center (y+0.5), extending 0.01 blocks on all six sides. This allows mobs to attack the entity from any side while the entity does not obstruct block placement or movement.
Recipe parsing flow:
Recipe JSON --> serializeRecipe() --> JsonElement
|
v
DSL Template (.rpml) --> TemplateParser --> TemplateNode (AST)
|
v
RecipeExtractor.extract()
|
v
RecipeData
(Ingredient[], Product[])
.rpml DSL template into an AST| Class | Responsibility |
|---|---|
RecipeParser |
Singleton core, manages template loading, recipe parsing, and fluid transfer |
RecipeParserAPI |
Static facade class, exposes all APIs externally |
RecipeData |
Parse result, contains inputs and outputs |
Ingredient |
Input material: id, count, unit, type |
Product |
Output product: id, count, unit, type, rate (probability) |
All APIs are called via RecipeParserAPI static methods.
| Method | Description |
|---|---|
getRecipeData(ResourceLocation, RecipeManager) |
Parse a single recipe |
getRecipeDataBatch(Collection, RecipeManager) |
Batch parse recipes |
isRecipeParsable(ResourceLocation, RecipeManager) |
Check if a recipe is parsable |
getParsableRecipes(String modId, String recipeType, RecipeManager) |
Get all parsable recipes of a given type |
// Find recipes that produce the target
List<RecipeParseResult> parseProduceRecipe(
ResourceLocation targetId, // Target item/fluid ID
QueryMode mode, // ITEM or FLUID
RecipeManager recipeManager
)
// Find recipes that use the target as input
List<RecipeParseResult> parseUsageRecipe(
ResourceLocation targetId,
QueryMode mode,
RecipeManager recipeManager
)
| mode | Match Logic (produce) | Match Logic (usage) |
|---|---|---|
ITEM |
Matches direct output + fluid-to-item transfer | Matches ingredients where unit=item |
FLUID |
Matches fluid outputs + reverse transfer matches | Matches ingredients where unit=fluid |
All async APIs are called via RecipeParserAPI static methods. Heavy recipe scanning and parsing work runs on dedicated worker threads (AsyncRecipeParser thread pool, 2 daemon threads), and results are delivered back to the server thread via MinecraftServer callback.
RecipeParserAPI.parseProduceRecipeAsync(
ResourceLocation.parse("minecraft:oak_planks"),
QueryMode.ITEM,
recipeManager,
results -> {
// This callback runs on the server thread -- safe to manipulate Minecraft objects
for (RecipeParseResult r : results) {
sendSuccess("Found recipe: " + r.getRecipeId());
}
},
errorMsg -> sendFailure("Query failed: " + errorMsg),
server
);
Async method signatures:
| Method | Description |
|---|---|
getRecipeDataAsync(id, mgr, cb, err, server) |
Asynchronously parse a single recipe |
getRecipeDataBatchAsync(ids, mgr, cb, err, server) |
Asynchronously batch parse recipes |
parseProduceRecipeAsync(id, mode, mgr, cb, err, server) |
Asynchronously find producing recipes |
parseUsageRecipeAsync(id, mode, mgr, cb, err, server) |
Asynchronously find usage recipes |
Ingredient
| Field | Type | Description |
|---|---|---|
id |
String |
Item/fluid/tag ID |
count |
double |
Quantity |
unit |
String |
"item" / "fluid" |
type |
String |
"solid" / "tag" / "fluid" |
Product
| Field | Type | Description |
|---|---|---|
id |
String |
Product ID |
count |
double |
Quantity |
unit |
String |
"item" / "fluid" |
type |
String |
"solid" / "tag" / "fluid" |
rate |
double |
Output probability (0.0 ~ 1.0, default 1.0) |
Recipe templates (.rpml files) are located in cloudworks/recipe_parser/templates/, named in the format {modid}_{recipetype}.rpml.
| Marker | Syntax | Description |
|---|---|---|
input |
<input,id=foo,count=1,unit=item,type=solid> |
Declares an input ingredient |
output |
<output,id=bar,count=1,unit=item,type=solid> |
Declares an output product |
object |
<object,id=myObj> |
Declares a JSON object node |
key |
<key,id=myKey> |
Dynamic JSON key traversal |
io_attribute |
<count,output_id=bar> |
Injects attributes into markers |
symbol |
<symbol,id=sym,input_id=keyIng> |
Declares a shaped crafting symbol |
patternline |
<patternline,id=line> |
Declares a shaped crafting pattern row |
duplicate |
<duplicate,id=dupX,structure=X> |
Declares a repeatable JSON object structure |
script |
<script,set_global_fluid_transfer=true> |
Global settings script |
optional |
<optional,id=opt> |
Optional field |
variable |
<variable,id=var> |
Variable reference |
Vanilla Crafting Table (Shaped)
{
"type": "minecraft:crafting_shaped",
<script,set_global_fluid_transfer=true,set_global_default_transfer_rate=250>
"pattern": [
<patternline,id=line>
],
"key": <object,id=keyObj>
<key,id=key>
<object,id=keyIng>
"item": <input,id=keyIng,count=1,unit=item,type=solid>
"tag": <input,id=keyIng,count=1,unit=item,type=tag>
<symbol,id=sym,input_id=keyIng>
<count,id=keyIng_count,input_id=keyIng>
<patternline,id=line,input_id=sym>
"result": <object,id=res>
"count": <count,output_id=res>
"id": <type,output_id=res>
"item": <output,id=res,count=1,unit=item,type=solid>
Vanilla Crafting Table (Shapeless)
{
"type": "minecraft:crafting_shapeless",
"ingredients": [
<object,id=ingStruct>
"item": <input,id=ingItem,count=1,unit=item,type=solid>
"tag": <input,id=ingItem,count=1,unit=item,type=tag>
<count,id=ingCount,input_id=ingItem>
<duplicate,id=ingStruct>
],
"result": <object,id=res>
"count": <count,output_id=res>
"id": <type,output_id=res>
"item": <output,id=res,count=1,unit=item,type=solid>
When a recipe outputs fluid, it can be converted to an equivalent item, allowing the recipe to be found via item ID as well.
Transfer Logic: Total fluid (mB) ÷ rate → rounded (per round strategy) × per-unit product quantity
Global Settings (configured via <script> in templates):
| Parameter | Type | Default | Description |
|---|---|---|---|
global_fluid_transfer |
boolean |
false |
Whether to enable fluid-to-item transfer |
global_default_transfer_rate |
double |
100 |
How many mB convert to 1 item |
global_default_transfer_result |
string |
null |
Default transfer result item ID |
global_default_transfer_extra_input |
map |
{} |
Extra ingredients |
global_default_transfer_float_round |
enum |
default |
round_up / round_down / default |
global_enable_template_config |
boolean |
true |
Whether to enable external template config files |
Recipe-Level Config (cloudworks/recipe_parser/templates_config/{modid}_{recipetype}.json):
{
"recipe:id": {
"enable_transfer": true,
"transfer_blacklist": ["fluid:to_skip"],
"methods": [{
"rate": 100,
"round": "default",
"extra_input": {"minecraft:item": 1},
"result": {"minecraft:output_item": 2}
}]
}
}
cloudworks/recipe_parser/config.json:
{
"version": "1.1.0-1.21.1",
"enable_update": true,
"force_update": false,
"update_ignore": ["minecraft_crafting.rpml"]
}
force_update=true → Skip all checks, release all filesenable_update=false → Skip updateversion matches → Skip updateupdate_ignoreConsoleSeeker outputs console logs to the in-game chat in real-time and provides a complete event pipeline for downstream mods to filter and process logs.
@CloudWorksConsoleSeeker // Forces ConsoleSeeker + API to be enabled
Or configure in cloudworks/console_seeker/config.json:
{
"enable_module": true,
"enable_api": false,
"enable_command_for_any_operator": true,
"list_type": "whitelist",
"player_list": [],
"max_log_length": 150,
"enable_timestamp": false
}
| Field | Type | Default | Description |
|---|---|---|---|
enable_module |
boolean |
true |
Whether to enable the ConsoleSeeker module |
enable_api |
boolean |
false |
Whether to enable the event API (annotations override this) |
enable_command_for_any_operator |
boolean |
true |
Whether to allow all OPs to use commands |
list_type |
string |
"whitelist" |
"whitelist" / "blacklist" |
player_list |
string[] |
[] |
Player name list |
max_log_length |
int |
150 |
Maximum log length (0 = no truncation) |
enable_timestamp |
boolean |
false |
Whether to show timestamps |
Log4j2 Logger
│
▼
ChatAppender (Log4j2 Appender)
│ Filters [CHAT] substring
▼
LogToChatManager
│ Filters by log level and player subscription
▼
ConsoleSeekerEventManager
│ Internal filter units → collects tagSet
▼
NeoForge EVENT_BUS
│ Dispatches ConsoleSeekerInfoEvent / WarnEvent / ErrorEvent
▼
ExternalLogFilter (subscribed by downstream mods)
│ Matches tagSet → parse() → onReceive()
▼
Downstream mod custom logic
Three independent event pipelines for different log levels:
| Event Class | Log Level |
|---|---|
ConsoleSeekerInfoEvent |
INFO |
ConsoleSeekerWarnEvent |
WARN |
ConsoleSeekerErrorEvent |
ERROR |
All events inherit from ConsoleSeekerLogEvent with the following fields:
| Field | Type | Description |
|---|---|---|
loggerName |
String |
Logger name |
level |
Level |
Log level |
message |
String |
Formatted message (ANSI color codes removed) |
timestamp |
long |
Unix timestamp (milliseconds) |
threadName |
String |
Thread name |
thrownString |
String |
Exception stack trace (null if no exception) |
tagSet |
Set<DirectedDeliveryTag> |
Directed delivery tag set |
Register internal filter units via ConsoleSeekerEventManager to have ConsoleSeeker actively filter logs:
// Implement the InternalFilterUnit interface
InternalFilterUnit myUnit = new InternalFilterUnit() {
public DirectedDeliveryTag getTag() {
return new DirectedDeliveryTag("mymod", "cpu_alert");
}
public boolean test(LogEvent event) {
return event.getLoggerName().startsWith("com.example");
}
};
// Register to the internal filter unit list
ConsoleSeekerEventManager.addInternalFilterUnit(myUnit);
Filter Rules:
Uses the "modid:tagName" format to prevent cross-mod conflicts:
// Create a tag
DirectedDeliveryTag tag = new DirectedDeliveryTag("mymod", "cpu_alert");
// Matching rules: empty tag matches everything, non-empty tag requires exact match
tag.matches(otherTag);
Downstream mods extend ExternalLogFilter<T> for custom log parsing:
public class MyCpuFilter extends ExternalLogFilter<Integer> {
public MyCpuFilter() {
super(new DirectedDeliveryTag("mymod", "cpu_alert"));
}
@Override
protected Integer parse(ConsoleSeekerLogEvent event) {
// Custom parsing logic
return Integer.parseInt(event.getMessage());
}
@Override
protected void onReceive(Integer value) {
// Handle the parsed result
System.out.println("CPU usage: " + value);
}
}
// Register to start receiving events
MyCpuFilter filter = new MyCpuFilter();
filter.register();
Workflow:
tagSet against the filter's tag (intersection matches, empty tag set matches everything)parse() converts the raw event to a custom type (returns the raw log string by default)onReceive() processes the parsed resultCommand prefix: /cloudworks. Requires OP level 4 (some commands).
/cloudworks recipe parse produce item [id] — Query recipes producing the specified item
/cloudworks recipe parse produce liquid [id] — Query recipes producing the specified fluid
/cloudworks recipe parse usage item [id] — Query recipes using the specified item
/cloudworks recipe parse usage liquid [id] — Query recipes using the specified fluid
/cloudworks recipe parsebatch <modid> <type> — Batch parse
/cloudworks recipe listtemplates — List loaded templates
/cloudworks console <info|warn|error> <on|off> — Toggle log output to chat
/cloudworks config console player_list add <player_name> — Add player to list
/cloudworks config console player_list remove <player_name> — Remove player from list
/cloudworks config console player_list query — View player list
/cloudworks status — View all module status
Output includes: RecipeParser template count, ConsoleSeeker enabled levels/API status/filter unit count.
src/main/java/com/cloudworks/api/
├── CloudWorksAPI.java # NeoForge mod entry point
├── annotation/
│ ├── CloudworksRecipeParser.java # Enables RecipeParser
│ └── CloudWorksConsoleSeeker.java # Enables ConsoleSeeker
├── command/
│ └── DebugCommand.java # Unified debug command entry
│
├── durableblock/ # DurableBlock module
│ ├── DurableBlock.java # Durable block base class
│ ├── DurableBlockEntity.java # Block entity replacement (LivingEntity)
│ ├── PersistentDurableBlock.java # Persistent durable block
│ ├── PersistentDurableBlockEntity.java # Code game protection entity
│ ├── DurableBlockDamageType.java # Damage type enum
│ └── jade/
│ └── DurableBlockJadePlugin.java # Jade mod integration plugin
│
├── recipeparser/ # RecipeParser module
│ ├── RecipeParser.java # Core singleton
│ ├── RecipeParserAPI.java # Static API facade
│ ├── AsyncRecipeParser.java # Async thread pool
│ ├── ApiSelfTest.java # Auto-test
│ ├── DebugOutputWriter.java # Debug JSON export
│ ├── dsl/ # DSL template engine
│ │ ├── Template.java, TemplateNode.java
│ │ ├── TemplateParser.java, TemplateTokenizer.java
│ │ ├── TemplateValidator.java, RecipeExtractor.java
│ │ ├── GlobalSettings.java, TemplateConfig.java
│ │ └── ...
│ └── model/ # Data models
│ ├── RecipeData.java, Ingredient.java, Product.java
│ └── QueryMode.java, RecipeParseResult.java
│
└── consoleseeker/ # ConsoleSeeker module
├── ChatAppender.java # Log4j2 Appender
├── LogToChatManager.java # Log-to-chat manager
├── ConsoleSeekerConfig.java # Config file management
├── ConsoleSeekerEventManager.java # Event pipeline core
├── ConsoleSeekerCommand.java # Command handler
├── InternalFilterUnit.java # Internal filter unit interface
├── ExternalLogFilter.java # External filter abstract class
├── DirectedDeliveryTag.java # Directed delivery tag
├── LogFilter.java # Log utility (truncation/color removal)
└── event/ # Event classes
├── ConsoleSeekerLogEvent.java # Base event class
├── ConsoleSeekerInfoEvent.java
├── ConsoleSeekerWarnEvent.java
└── ConsoleSeekerErrorEvent.java
This project is open-sourced under the GNU General Public License v3.0. See LICENSE for the full text.
CloudWorks API — Unified Mod Development Interface Copyright (C) 2026 CloudWorks Team
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.