API - AntonIT99/Flans-Mod-Ultimate-2.0 GitHub Wiki

The com.flansmodultimate.api package is the supported way for other mods to interact with Flan's Mod Ultimate. Everything outside it is internal and may change without notice.

Part hitboxes for other entities (API 0.14, Forge 1.20.1 and NeoForge 1.21.1)

An entity that is not a driveable can be hit as one: by the part, with the box, hit points and armour each part of a driveable definition has, turned with the entity and its turret. The NPC module uses this for NPCs drawn as vehicles, planes and mechas; nothing in it depends on Custom NPCs.

Type Gives
FlansPartBodies.create(owner, type) The IPartBody of a vehicle, plane or mecha definition for an entity, or empty when the definition gives no part a box. Either side.
IFlanPartTarget Implemented by the entity, on top of IFlanDamageModel: flansmodultimatePartBody() hands out the body to hit, or empty to be hit as one bounding box, and flansmodultimateHurtPart(part, source, amount) receives each hit.
IPartBody The parts (getParts(), getPart(name)), their boxes (setBox, setEnabled), hit points (damage, setHealth, restore, tickFire, getHealth, getMaxHealth), the pose (setPose), saveState/loadState for saving, and signature() to notice changes.
PartBox One part's box in the static model preview frame (blocks, Y up, forward -Z, from the model origin), its hit points and its penetration resistance.
PartState A snapshot of one part: name as in type files, the box in use and the definition's, whether it is custom, enabled, turret-mounted, its hit points left and whether it is broken or burning.
PartBodyPose World position of the model origin, the vanilla yaw the model faces, its scale and the turret's MountAim. The body stays level.

While the entity hands out a body, Flan rounds and explosions ignore its own bounding box. A round is traced against the part boxes through the same hull and turret transforms a driveable uses, strikes the nearest part, and is resolved against the armour of that part and face when flansmodultimateModelArmour() is true and the damage model is a driveable. A blast is shared out between the parts it reaches, each behind its own plate, as on a driveable. The result reaches the entity through flansmodultimateHurtPart, in the hit points of the definition; the entity decides how to take it, usually by hurting itself and handing what gets through to IPartBody.damage. Breaking a part breaks the parts that hang on it, and a broken part lets rounds through unless setBrokenPartsSolid(true). Melee, vanilla projectiles and everything else still use the entity's own box.

Part boxes may reach far past the entity's bounding box, as a ship's do; the entities wearing a body are found without a box query, so nothing has to be enlarged. Set the pose once per tick on both sides, after the entity has moved. The server decides hit points: bring saveState() or your own summary across to clients, which need the broken parts to follow rounds faithfully. setOutlined(true) draws the boxes on clients outside debug mode. setSolid(true) makes other entities collide with the boxes as with a driveable's hull: they walk into them, stand on them and are carried along. The wearer, its riders and what it rides pass through.

Teams and player classes (API 0.13, Forge 1.20.1 and NeoForge 1.21.1)

FlansModApi.getTeams() lists the teams the loaded content packs define, and getTeam(String shortName) looks one up. Both leave out the built-in spectators. The definitions are read-only content data, readable on either side; who plays for a team and its score belong to the running round and are not exposed.

Interface Gives
ITeam Everything IContentType gives (name, short name, content pack), getTeamColour() as 0xRRGGBB, getTextColour(), getClasses() in definition order, and getArmour(EquipmentSlot): the armour the team issues for a slot.
IPlayerClass getArmour(EquipmentSlot): the class's own piece for a slot, createStartingItems(): new stacks in definition order, with guns carrying their attachments and default ammunition, and getSkinOverrideTexture().

A member wears the class's armour piece for a slot, and the team's where the class names none. Armour and starting-item stacks are new each call; a slot or item that is not loaded yields an empty stack or is left out. getSkinOverrideTexture() is client only and returns null on a dedicated server. The texture is not validated: it may be missing, and may use the square skin layout or the old 2:1 one. The NPC module uses these types for the Flan Soldier folder of its model picker.

FlansEquipment.acceptsAmmunition(weapon, ammunition) tells whether a gun can be reloaded with a stack: ammunition the gun takes in its current fire mode that still holds rounds. It consumes nothing and is safe on either side; loadMagazine accepts exactly the stacks it approves. FlansEquipment.isDeployable(weapon) tells whether a gun is a deployable one, placed in the world rather than fired from the hand.

Ship classification (API 0.12, Forge 1.20.1 and NeoForge 1.21.1)

IDriveableType.isShip() exposes the classification shared by the Ships creative tab and NPC model browser. It recognizes water-only hulls, legacy plane-based ships with disabled pitch and roll controls, and definitions marked EpicShip. Amphibious ground vehicles and seaplanes with flight controls remain in their usual groups. This read-only query is safe on either side and leaves getDriveableKind() unchanged.

Tracked driveables (API 0.11, Forge 1.20.1 and NeoForge 1.21.1)

IDriveableType.isTracked() tells whether a driveable runs on tracks and can turn on the spot, as a tank does. It is false for wheeled vehicles, planes and mechas. The NPC module uses it to pick how a vehicle NPC steers.

Mounts, aimed muzzles and models in motion (API 0.10, Forge 1.20.1 and NeoForge 1.21.1)

These additions let an entity that is not a Flan vehicle aim, fire from and be drawn as one in motion. The NPC module is their first consumer; none of them depends on Custom NPCs.

Aim convention. MountAim(yaw, pitch) says where a turret, a seat gun or an AA gun points relative to the hull it stands on, in degrees and in Minecraft's conventions: yaw positive to the right, pitch positive downwards. MountAim.REST points straight ahead and level.

Mounts. FlansEntityTypes.getMounts(type) lists what a definition can aim, ordered by seat index: every seat of a driveable, the driver first, or the single gun of an AA gun. A WeaponMount gives the seat index, whether it is the driver's and whether it sits on the main turret, its minimum and maximum aim, its traverse and elevation speed in degrees per tick, where its occupant sits and the point a turret seat is carried round, both in the static model preview frame, and the gun definition it mans. turnsFreely(), restYaw(), clamp(aim) and positionAt(turretYaw) do the arithmetic; clamp keeps a limited traverse contiguous even when its arc straddles the rear. Readable on either side.

Aimed muzzles. FlansProjectiles.getMuzzles(model, secondary, sequence, alternate, aim) returns the same muzzles as the four-argument form, with those on the turret carried round TurretOrigin and elevated with the gun, and an AA gun's barrels turned and elevated round the gun. Muzzles on the hull do not move. The result matches where the driveable entity itself puts them, except that the gun elevates round TurretOrigin: the barrel pivot of the loaded model is only known to clients.

Models in motion. FlansModelPreviews.renderWorld(..., pose) draws a world model from a ModelPose: turret aim, one aim per seat, throttle, blocks travelled along the hull this tick, degrees the hull turned this tick, doors, gear, wings, engine, and a running shot count with the barrel that fired. Pass the pose of the entity's current tick; it may be passed again on every frame of that tick. The renderer keeps the history per entity, derives wheel, steering, track, propeller, rotor, leg and recoil motion from successive poses, and interpolates between ticks. ModelPose.REST is the model at rest with its engine running. Level of detail, the GPU cache and impostors work as for the static form; an impostor shows the model at rest. Client render thread only.

Armour pose steps. FlansEquipmentRender.addArmorPoseStep(step) registers an ArmorPoseStep, run on every Flan armour model just before each piece is drawn, after the wearer's own model pose has been copied to it. A step may scale and move the parts freely: they are reset before the next piece. Register once, during client setup.

NPC tracking range. FlansModApi.getNpcTrackingRange() returns the range, set by the server and synchronised to clients, within which NPCs using the Flan distance settings are sent to clients. Addons use it as the upper limit of NPC combat ranges.

Damage stand-ins, reload cycles and holder animations (API 0.9, Forge 1.20.1 and NeoForge 1.21.1)

These additions let an entity that is not a Flan vehicle be damaged, reload and animate like one. The NPC module is their first consumer; none of them depends on Custom NPCs.

Damage stand-ins. An entity implementing IFlanDamageModel returns the AA gun or driveable it stands in for from flansmodultimateDamageModel(), or empty for ordinary living-entity damage. Flan ammunition and explosions then pick their damage accordingly: against planes for a plane definition, against vehicles for any other driveable, and the base damage for an AA gun, as a placed AA gun takes. While flansmodultimateModelArmour() returns true (the default), kinetic rounds and shaped charges are also resolved against the driveable's core armour with the same penetration rules as a hit on the real hull. A stand-in without part boxes is struck on the face chosen from the round's direction relative to the entity's body yaw; one that hands out an IPartBody (API 0.14, above) is struck on the part and face the round actually met. Armour that stops a round deals no damage and ends its penetration. Both methods are read wherever Flan resolves a hit: on the server thread, and on a client following a round, so they must answer on both sides.

Reload cycles. FlansEntityTypes.getReloadCycle(type, secondary, ammunition) returns an optional ReloadCycle: how many triggers the definition fires before it reloads with that ammunition, the delay between triggers and the reload time, both in ticks, and the tick at which a main gun plays its chambering sound, when it declares one. It follows the rules of the placed entity. An AA gun empties every ammunition slot, counting one trigger per volley or per barrel when it alternates. A driveable bank reloads after the smaller of its declared reload rounds and the rounds one item holds. Reload time includes the ammunition's reload multiplier where the real bank applies it, and is never shorter than the trigger delay. Readable on either side after content loading; it is empty for unsupported definitions and for banks without a weapon. The caller keeps its own timers. FlansProjectiles.playReloadSound(source, platform, secondary) and playChamberSound(source, platform) send the matching content sounds on the server thread and return whether one was declared. FlansProjectiles.getRoundsPerItem(stack) reads how many rounds one item of Flan ammunition holds, or zero for another item.

Holder animations. FlansEquipment.animateShot(holder, hand) raises a held gun's aim pose and plays its shot animation and muzzle flash model for clients that see the holder, for callers that launch their own projectile on the gun's behalf; FlansEquipment.fire already does this. animateReload(holder, hand, reloadTicks) and animateMelee(holder, hand) play the gun's reload and melee animations for a non-player holder. playMeleeSound(holder, hand) sends the gun's melee sound with its authored range and returns whether it declares one. All four act on the server thread and do nothing when the hand holds no Flan gun. IEquipmentPolicy.flansGunShotAnimations(), default true, lets an entity suppress the shot animation while keeping its aim pose, which stays governed by flansWeaponAnimations().

Living presentation and engine audio (API 0.8, Forge 1.20.1 and NeoForge 1.21.1)

ILivingVisualEffects is an optional living-entity rendering policy. Implement flansmodultimateHurtFlash() and flansmodultimateDeathRotation() to control the red hurt/death overlay and death tipping. Both default to true. They are read on the render thread and do not change entity damage/death timers or gameplay. Renderers that override vanilla death transforms may need their own integration.

FlansEntityTypes.getEngineSound(type) and getEngineIdleSound(type) return optional EngineSound records containing the sound event ID, effective audible range in blocks and authored interval in ticks. Mecha stomps are excluded. Movement timing prefers EngineSoundLength, then StartSoundLength, then 20 ticks. Idle falls back through IdleSound, StartSound and EngineSound with the corresponding length or 20 ticks. Range incorporates server pack-range policy. getEnginePitch(type, movement) evaluates authored pitch from normalized speed, clamped to 0.5-2. Read these on either side after content loading.

FlansEntitySounds.getSoundLength(eventId) returns measured Flan asset duration at pitch 1, independent of type timer overrides, or empty for unknown/non-Flan events. getDefaultEngineRange() reads the synchronized server vehicle radius.

FlansEntitySounds.playEngineSound(source, sound, instanceId) sends one following, cancellable clip on the server thread and returns whether audio was sent. Namespaced events from other mods/resource packs work; unqualified paths use flansmod. Clients ignore unavailable audio events. The overload with variablePitch = true reads optional IEngineSoundSource.flansmodultimateEnginePitch() each client tick; absent or invalid values use normal pitch. Callers own repeat timing and pitch-duration adjustment and use a fresh UUID per clip. stopEngineSound(source, instanceId) cancels throughout the source's dimension, including listeners it moved away from. Invalid IDs, nonpositive/nonfinite ranges and silent/dead sources do not play. Tracked clients follow the source; other listeners hear a positional clip at normal pitch. These methods load no client classes on the server. NPC editor and persisted channel settings remain internal to the NPC module.

License

Unlike the rest of Flan's Mod Ultimate (CC BY-NC-SA 3.0), the API is licensed under the MIT License. You may compile against it, implement it where allowed, copy from it and distribute addons built on it, including commercially, as long as you keep its copyright notice where you copy its code. Your addon can use any license.

Getting the API

The API is published as its own artifact, separate from the mod:

Maven coordinates com.flansmodultimate:flansmodultimate-api:<minecraft version>-<API version>, such as 1.20.1-0.6 for Forge or 1.21.1-0.6 for NeoForge
Classifiers none (classes), sources, javadoc. The Forge 1.20.1 classes are reobfuscated; the NeoForge 1.21.1 classes use Mojang names.

It is served from the Maven repository https://antonit99.github.io/flansmodultimate-maven, which also lets you download the jars directly.

Compile against it and let the installed mod provide it at runtime. With ForgeGradle:

repositories {
    maven { url = 'https://antonit99.github.io/flansmodultimate-maven' }
}

dependencies {
    compileOnly fg.deobf('com.flansmodultimate:flansmodultimate-api:1.20.1-0.6')
    // To run Flan's Mod Ultimate in your development client, also add the mod itself:
    // runtimeOnly fg.deobf(<the Flan's Mod Ultimate mod jar>)
}

The API version is independent of the mod version: the minor version grows with additions, the major version with breaking changes.

To try unreleased API changes, build the jars from the repository with gradlew apiJar apiSourcesJar apiJavadocJar; they are written to build/libs.

Runtime physics factors

FlansModApi.getGravityFactor(Level) and getDragFactor(Level) return the effective factors for that level. Every dimension defaults to 1, including the Overworld, Nether and End. At 1, authored physics is unchanged; 0 removes the relevant force from Flan's simulated entities. These factors do not affect vanilla entities or other mods' physics. Client calls use the server's synchronized settings.

Call FlansModApi.setGravityFactor(ServerLevel, double) or setDragFactor(ServerLevel, double) on the server thread to override one dimension with a factor from 0 to 10. clearGravityFactor(ServerLevel) and clearDragFactor(ServerLevel) remove an override, returning its dimension to 1. Each returns true if the config changed. Changes take effect immediately, are saved to the common TOML, and are sent to clients. A planet or atmosphere mod can configure each dimension independently.

Forge 1.20.1 and NeoForge 1.21.1 both have source API 0.15. Publish each branch's API artifact before depending on its coordinates. Version branches publish independently; check their source/API version before using a new entry point.

Paintjob variants and preview bounds (API 0.5)

IContentType.getPaintjobVariants() returns an immutable list of PaintjobVariant records ordered by ID, with default ID 0 first. Nonpaintable definitions return an empty list. Each record contains the ID, declared display name (falling back to the texture name), and body texture. IDs and names are available on either side after content loading; textures are client-only and null on a dedicated server.

Client render-thread callers can use FlansModelPreviews.getBounds(type) to measure the static model's rendered geometry, including legacy transforms and model scale. The optional AABB uses the same block coordinates as render, before the driveable resting-height offset. Measurements are cached until model reload; missing, unsupported or invalid geometry returns an empty optional. To frame a complete vehicle preview, combine this with the entity dimensions and IDriveableType.getBounds() for the part collision boxes.

Looking up definitions

FlansModApi gives access to the definitions of the loaded content packs. Content packs are read while Flan's Mod Ultimate is constructed, so every definition is available from the first registry event onwards, on both the client and the server.

Method Returns
getType(String shortName) The definition with that short name, which is also the path of its item id, if any.
getTypes() Every definition that has an item, unmodifiable and in no particular order.
getContentPacks() The loaded content packs (IContentPack): packaged ones first, then the user's in alphabetical order.
getTeams() The teams (ITeam) the content packs define, unmodifiable, without the built-in spectators.
getTeam(String shortName) The team with that short name, if any.

An IContentPack gives the pack's name, the path it is loaded from (read it, never modify it) and whether it is official content from the official-packs companion mod.

Definition types

Interface Implemented by Adds
IContentType Every definition Short name, name, description, content pack (IContentPack), item, default texture.
IAAGunType AA guns Nothing yet: use it to recognise AA guns.
IDriveableType Vehicles, planes, mechas Kind (DriveableKind), resting height, core and overall bounds.
IMechaType Mechas Declared width and height.
ITeam Teams Colours, classes and issued armour.
IPlayerClass Player classes Armour, starting items and skin.

IDriveableType measures in blocks, in the driveable's local frame before rotation, with the origin at the entity position and Y up. X is the lateral axis and Z the fore-aft one; which end of Z is the front differs between planes and other driveables, so use extents rather than signs.

  • getRestingHeight(): the height of the origin above level ground when the driveable rests on its wheels, as its suspension places it, or its YOffset when it has no wheels. It can be negative when the wheels sit above the origin, as with some flying boats.
  • getCoreBounds(): the collision box of the core part, which is the main body.
  • getBounds(): the box enclosing every part's collision box.

Seats

ISeat is a seat of a driveable: the driver's position or a passenger's, possibly manning a gun. It gives the seat index, whether it is the driver's seat, the entity sitting in it, the driveable entity and its IDriveableType, the aim relative to the driveable in degrees, and the seat gun's definition, rounds, magazine size, reload ticks and loaded ammunition name. The gun state is synchronised to clients, so it can be read on both sides.

A rider's seat is the entity it rides: player.getVehicle() instanceof ISeat seat.

Projectiles

Projectiles fired by Flan's weapons implement IBullet, which gives who fired them, their ammunition definition and, when known, the definition of the weapon that fired them. In a damage handler, check source.getDirectEntity() instanceof IBullet bullet to tell Flan's shots apart from other projectiles.

Addon projectile launches (API 0.6, Forge 1.20.1 and NeoForge 1.21.1)

FlansProjectiles lets another mod launch real Flan bullets and grenades without depending on internal weapon, type or entity classes. All mutations require the owning server thread; unsupported ammunition or invalid launch input returns an empty result. The API does not consume inventory or choose attack timing.

  • isProjectile(stack) and isGrenade(stack) recognize ammunition on either side.
  • getSources(...) inspects an ammunition template, held/mounted weapon, platform, selected bank and weapon-statistics switch without spawning anything. Its ProjectileSources reports whether damage, spread and velocity come from the caller, weapon or ammunition, including kinetic damage and platform overrides, and whether content reserves firing audio. Mixed belts report CALLER when any round still needs that fallback. Readable on either side after content loading, this supports editors that retain inactive caller values in read-only form.
  • prepare(...) resolves a shooter, ammunition template, held gun, optional mounted weapon and platform definitions, bank and ProjectileParameters (belt position, the weapon-statistics switch and fallback weapon statistics). It returns a ProjectileShot with velocity in blocks/tick, gravity in blocks/tick squared and air velocity retention, including dimension factors.
  • ProjectileShot.launch(origin, direction, throwSound) returns an immutable list of spawned entities. It includes weapon pellet counts and launches entity bullets even when the original gun uses hitscan. Positions and directions use world coordinates.
  • getMuzzles(model, secondary, sequence, alternate) selects an immutable list of WeaponMuzzle records for AA guns and driveables, including alternating banks and measured individual barrels. Offsets use the static model-preview frame: blocks, Y up, forward -Z, before resting height, caller scale and body rotation. A muzzle can identify its mounted gun and supplies immutable shooting-particle descriptors.
  • playShootSound(...) selects the content firing sound, including held-gun attachments, and returns whether it sent one. shootParticles(...) broadcasts authored particles from the supplied world muzzle position, with a small flame fallback. Callers choose whether to invoke these presentation functions.

The NPC module is the first consumer. Its switches, Custom NPCs mixins, editor, attack timing and targeting remain module-specific. The reusable projectile construction and content presentation live behind this API. The API remains experimental; these additions are present on Forge 1.20.1 and NeoForge 1.21.1. Each branch's API artifact requires a separate publication before use.

AA and driveable property inspection (API 0.6, Forge 1.20.1 and NeoForge 1.21.1)

FlansEntityTypes.getProperties(type, secondary) returns an optional immutable EntityTypeProperties snapshot after content loading, on either logical side. It does not create a live vehicle or mutate gameplay. An overload accepts mountedWeapons: false inspects platform weapon defaults directly, for consumers that disable model muzzle/gun resolution.

The snapshot exposes optional total hull HP, AA target radius, unloaded mecha walking speed in blocks/tick with a unit-speed engine, mecha reach, submerged operation, fall-damage enablement and selected bank cadence in ticks. It includes declared idle/step sound IDs and the bank's distinct Weapon definitions, with damage, spread, optional velocity, pellets and firing sound. Mounted gun defaults include platform damage multipliers; ammunition and held attachments are not folded into these type readouts. Empty values deliberately leave consumer defaults available.

The NPC consumer converts these properties into Custom NPCs units, applies a reversible server-owned overlay, and retains stored NPC defaults in save/menu data. Widget IDs, switch precedence, numeric limits and NPC cadence adaptation remain in the NPC module because they are specific to Custom NPCs.

Equipped-item actions and inspection (API 0.6, Forge 1.20.1 and NeoForge 1.21.1)

FlansEquipment inspects Flan item stacks through EquippedWeaponProperties and EquippedArmorProperties. API 0.15 adds invisibility to the armor properties: whether the armor hides its wearer's body while staying visible itself. Weapon inspection uses a detached preview, honors stack attachments, and prefers usable loaded ammunition over supplied preview ammunition. It reports native times in ticks, speed in blocks/tick (zero denotes hitscan), Flan spread units, pellet count and generic damage estimates. Actual impacts retain target and belt-round rules. It also reports automatic fire and minigun spin thresholds, and an Impact record for the selected ammunition: knockback share, hitbox size, gravity, explosion radius, and entity/block hit sounds (with a flag when block impacts use the struck material's sound). getLoadedAmmunition returns a detached magazine copy; getLoadedRoundIndex reads its next belt position without consuming ammunition.

Server-thread callers can use loadMagazine after their reload timer, and fire to execute a held gun or throwable gun. Successful actions consume real ammunition or remove one thrown weapon; projectile ownership and damage credit stay with the living holder. Native gun fire retains hitscan when no velocity is declared. These actions do not create a player proxy. The caller owns targeting, charge, reload and trigger cadence; hasLoadedRound provides a nonconsuming availability check. loadMagazine fills available empty/spent slots from the supplied spare stack and honors spent-magazine drops. playReloadSound broadcasts the native reload sound. Firing honors muzzle-flash settings and suppressed/last-round/distant audio.

Living entities may implement IEquipmentPolicy to gate Flan armor effects, held-shield behavior, weapon poses and armor limb animation separately. All four policies default to enabled. API 0.9 adds a fifth, flansGunShotAnimations(), described above. Entities without the policy retain normal behavior. The client-only FlansEquipmentRender.applyWeaponPose lets a custom humanoid animation pipeline apply equipment poses after its own animation.

NPC widget mappings, armor protection previews, vanilla weapon dispatch and the Custom NPCs-specific NpcWeaponAdapter stay in the NPC module. The main mod and public API have no Custom NPCs dependency.

Client: model previews

com.flansmodultimate.api.client.FlansModelPreviews draws static previews of AA gun and driveable models, for a GUI, a custom entity or a block entity. Call it on the client render thread only.

  • render(type, poseStack, vertexConsumer, light, overlay, red, green, blue, alpha) draws the model with its origin at the pose origin, facing -Z like a vanilla entity model, with Y up. The model is at rest: turrets face forward, doors are closed and gear is down. Lift a driveable by its resting height to stand it on the ground. The tint is multiplied with the definition's own colour. All render passes go to the one buffer given, so glowing parts are drawn like the others. It returns false when the definition has no model to preview.
  • getRenderType(type, texture) returns the render type the model should be drawn with, honouring the client's rendering options.

API 0.3 on Forge 1.20.1 and NeoForge 1.21.1 also provides renderWorld(type, entity, partialTick, entityPose, renderOffset, texture, poseStack, buffers, light, overlay, red, green, blue, alpha) for a static model used as an entity's ordinary body. It uses the shared GPU cache, projected-size detail reduction, supported fancy-track envelopes and eligible distant driveable impostors. The model origin and facing match render.

Pass the actual body skin, including custom skins, and preserve the packed overlay for damage flashes. entityPose is a snapshot taken on entry to the renderer, before living-model rotation, scale and offsets; renderOffset is the renderer's displacement from the interpolated entity position, in world blocks. poseStack holds the restored model origin, with Y up and facing -Z. History uses weak entity keys and is cleared on model reload.

Call this only for the ordinary body pass. Outlines, glint, invisible bodies and extra texture layers must keep their original consumer. The GPU cache retains its normal compatibility checks. Orthographic menu previews keep full detail; nonstandard buffer sources keep geometry. Mechas, partially transparent models, mirrored/nonuniform scales and sheared transforms keep exact geometry instead of impostors. NPC size, tilt, tint and damage overlays are retained, and tint does not create a separate atlas entry. The existing single-consumer render method remains available.

Depending on Flan's Mod Ultimate

Most addons work with or without Flan's Mod Ultimate installed. To make it an optional dependency:

  1. Declare it in your mods.toml, so it loads before your mod when present:

    [dependencies.yourmodid](/AntonIT99/Flans-Mod-Ultimate-2.0/wiki/dependencies.yourmodid)
        modId="flansmodultimate"
        mandatory=false
        versionRange="[2.1,)"
        ordering="AFTER"
        side="BOTH"
    
  2. Check that it is loaded before touching the API:

    if (ModList.get().isLoaded("flansmodultimate"))
        FlansCompat.init();
    
  3. Keep every reference to com.flansmodultimate.api inside classes such as FlansCompat that are only loaded after that check. The JVM resolves the types a class uses when it loads that class, so a single API type in a class that is always loaded crashes the game when Flan's Mod Ultimate is missing. Do not use API types as fields, parameters or return types of such always-loaded classes either.

  4. Keep client API calls (com.flansmodultimate.api.client) in client-only classes, as with any client code.

Make it mandatory=true instead if your mod cannot work without Flan's Mod Ultimate; the rules above then only matter for the client package.

Check definitions from registry events onwards, never from your mod's constructor: mods are constructed in parallel, so the content packs may not be read yet.

Stability

Every API type carries a JetBrains @ApiStatus annotation, which IDEs such as IntelliJ IDEA show as a warning where you use it:

Annotation Meaning
@ApiStatus.Experimental May still change between versions. All of the API is experimental for now.
@ApiStatus.NonExtendable Implemented by Flan's Mod Ultimate only. Use it, do not implement it.

Every API interface is currently non-extendable: Flan's Mod Ultimate implements them, other mods use them.

The API only grows by addition. Members are deprecated before they are removed, and stay at least until the next major version.

NPC distance integration (API 0.4)

IFlanNpcDistance is an experimental common API contract for NPC adapters. It exposes whether an NPC uses the Flan NPC distance settings, its saved explicit opt-in, and a server-thread setter for that opt-in. Implementations must persist and synchronize the explicit flag. Flan model recognition may independently enable the settings.

FlansModApi.refreshEntityTracking(ServerLevel) requests a reevaluation of player tracking on the next server tick after an addon changes distance classification. Both methods belong on the server thread; distance queries are safe on either entity-owning thread. Custom NPCs classes remain entirely in the NPC module.

These distance contracts were introduced in API 0.4. Forge 1.20.1 and NeoForge 1.21.1 now both have source API 0.6. API artifacts still require an explicit separate publication before depending on their new Maven coordinates.

Internal registry source migration (Forge 1.20.1 and NeoForge 1.21.1)

The mod entry point delegates registry definitions and startup helpers to smaller classes. Internal entity suppliers formerly accessed as FlansMod.bulletEntity (and the other entity fields) now live in com.flansmodultimate.FlansModEntities. Block and block-entity suppliers now live in com.flansmodultimate.FlansModBlocks, item suppliers in com.flansmodultimate.FlansModItems, and particle suppliers in com.flansmodultimate.FlansModParticles; use these owners directly. Built-in SOUND_* name constants now live in com.flansmodultimate.FlansModSounds. Texture constants (DEFAULT_BULLET_TEXTURE, DEFAULT_BULLET_TRAIL_TEXTURE, FALLBACK_TEXTURE, and TEXTURE_*) now live in com.flansmodultimate.FlansModTextures. Menu suppliers such as FlansMod.gunWorkbenchMenu now live in com.flansmodultimate.FlansModMenus. Source integrations using these internal fields must update their imports and field owners. Registry IDs, saved-world identities, content-pack formats, and the supported com.flansmodultimate.api contracts are unchanged.