Skip to content
32bitx64bit edited this page Jun 12, 2026 · 26 revisions

Temperature API (Fabric 1.20.1) — Core APIs

A lightweight library to handle temperature related needs.

  • Loader: Fabric
  • Minecraft: 1.20.1
  • Java: 17+

Contents:

  • TemperatureAPI (Stable)
  • HumidityAPI (stable)
  • BiomeAPI (with biomes.json format and where to put it) (Stable)
  • BodyTemperatureAPI (passive body temp change, with wetness and resistances) (Stable)
  • SoakedAPI (10s wetness when in water or in the rain) (Stable)
  • TemperatureResistanceAPI (simple heat/cold comfort expansion) (Stable)
  • BlockThermalAPI (Dynamic heating / cooling via a block) (Stable)
  • Indoor Climate System (New):
    • BlockInsulationAPI (per-block insulation, default 0.3)
    • BlockSetpointAPI (thermostatic emitters that hold a target temperature)
    • IndoorClimateAPI (indoor temperature lag model)
    • RoomAPI (enclosure / insulation read API)
  • Notes and troubleshooting

TemperatureAPI

Overview

  • Purpose: Read the world temperature at a position or player, with a simple API for:
    • Full ambient temperature (biome + season + diurnal + indoor lag + block sources)
    • Component-only values you can combine yourself:
      • Biome base temperature (no adjustments)
      • Seasonal offset
      • Diurnal/day-night offset
      • Raw outdoor environment (biome + season + diurnal; excludes indoor lag and block sources)
      • Effective environment (the above, after indoor insulation lag; excludes block sources)
  • Indoor climate:
    • Inside enclosed, insulated structures the ambient (biome + season + diurnal) term lags behind the outdoors instead of tracking it instantly. Block thermal sources (campfires, heaters) are local hotspots applied on top and are not lagged.
    • This is automatic: registering block insulation (or relying on the 0.3 default) and reading getTemperatureCelsius is all that's required. See the Indoor Climate System sections below.
  • Units:
    • Celsius base APIs with helpers to format in Celsius or Fahrenheit
  • Return values:
    • Numeric APIs return double
    • Formatted APIs return strings like “20.0°C”
    • Numeric APIs return NaN if inputs are null

Core methods

  • Formatted strings:
    • TemperatureAPI.getTemperature(World, BlockPos, TemperatureAPI.Unit)
    • TemperatureAPI.getTemperature(World, BlockPos, boolean fahrenheit)
    • TemperatureAPI.getTemperature(World, BlockPos, String unit)
    • Player overloads available for all the above
  • Numeric full ambient (biome + season + diurnal + indoor lag + blocks):
    • TemperatureAPI.getTemperatureCelsius(World, BlockPos)
    • TemperatureAPI.getTemperatureCelsius(PlayerEntity)
  • Component-only (for control logic):
    • TemperatureAPI.getBiomeBaseCelsius(World, BlockPos)
    • TemperatureAPI.getSeasonalOffsetCelsius(World, BlockPos)
    • TemperatureAPI.getDiurnalOffsetCelsius(World, BlockPos)
    • TemperatureAPI.getEnvironmentCelsius(World, BlockPos) // raw outdoor: biome + season + diurnal; excludes indoor lag and block sources
    • TemperatureAPI.getEffectiveEnvironmentCelsius(World, BlockPos) // biome + season + diurnal AFTER indoor lag; excludes block sources
    • Player overloads exist for all the above component methods

Basic usage

  • Show a formatted temperature string at a position (full ambient):
import gavinx.temperatureapi.api.TemperatureAPI;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public class ExampleReadFullAmbient {
    public static String at(World world, BlockPos pos) {
        // Returns "20.0°C" (includes indoor lag, blocks, season, diurnal)
        return TemperatureAPI.getTemperature(world, pos, TemperatureAPI.Unit.CELSIUS);
    }

    public static String atF(World world, BlockPos pos) {
        // Returns "68.0°F"
        return TemperatureAPI.getTemperature(world, pos, true);
    }
}
  • Get numeric Celsius (full ambient):
import gavinx.temperatureapi.api.TemperatureAPI;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public class ExampleReadNumeric {
    public static double readC(World world, BlockPos pos) {
        // Includes biome + season + diurnal + indoor lag + block sources
        return TemperatureAPI.getTemperatureCelsius(world, pos);
    }
}

Component-only usage

  • Read biome-only, raw outdoor, and effective (lagged) environment:
import gavinx.temperatureapi.api.TemperatureAPI;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public class ExampleComponents {
    public static double biomeOnly(World world, BlockPos pos) {
        return TemperatureAPI.getBiomeBaseCelsius(world, pos);
    }

    public static double seasonalOffset(World world, BlockPos pos) {
        return TemperatureAPI.getSeasonalOffsetCelsius(world, pos);
    }

    public static double diurnalOffset(World world, BlockPos pos) {
        return TemperatureAPI.getDiurnalOffsetCelsius(world, pos);
    }

    public static double rawOutdoor(World world, BlockPos pos) {
        // biome + season + diurnal; NO indoor lag, NO block sources
        return TemperatureAPI.getEnvironmentCelsius(world, pos);
    }

    public static double effectiveEnvironment(World world, BlockPos pos) {
        // biome + season + diurnal AFTER indoor lag; excludes block sources.
        // Equals getEnvironmentCelsius when outdoors.
        return TemperatureAPI.getEffectiveEnvironmentCelsius(world, pos);
    }
}

Building an actuator that should hold a target temperature (AC/heater/thermostat)? Prefer BlockSetpointAPI (below) over a hand-rolled BlockThermalAPI controller. BlockSetpointAPI is insulation- and enclosure-aware, combines correctly with other emitters, and won't fight its own contribution. The BlockThermalAPI controller pattern below remains valid for a "dumb" fixed-delta actuator.

Implementing a target-temperature controller with BlockThermalAPI (legacy pattern)

  • Use raw outdoor environment (biome + season + diurnal) as the baseline, so your controller doesn’t “fight” its own block contributions.

  • Compute the difference to your target and emit a BlockThermalAPI.ThermalSource with that delta.

  • Simple AC controller that returns a ThermalSource:

import gavinx.temperatureapi.api.TemperatureAPI;
import gavinx.temperatureapi.api.BlockThermalAPI;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public class ExampleACController {
    public static BlockThermalAPI.ThermalSource compute(World world, BlockPos pos) {
        // Excludes block sources to reduce feedback oscillations
        double envC = TemperatureAPI.getEnvironmentCelsius(world, pos);

        // Target 25°C
        double target = 25.0;
        double error = target - envC; // positive => needs heating; negative => cooling

        // Clamp to a reasonable power range
        double deltaC = Math.max(-8.0, Math.min(8.0, error));

        int range = 5;
        int dropoff = 10; // cosine falloff; clamped to 15 internally

        return new BlockThermalAPI.ThermalSource(
                deltaC,
                range,
                BlockThermalAPI.OcclusionMode.FLOOD_FILL,
                dropoff,
                BlockThermalAPI.FalloffCurve.COSINE
        );
    }
}
  • Use a dynamic provider so your block adapts every tick (or when queried):
import gavinx.temperatureapi.api.BlockThermalAPI;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public class ExampleACProvider {
    public static void registerACProvider() {
        // Provide a maxRangeHint that covers your range + dropoff
        BlockThermalAPI.register((World world, BlockPos pos, BlockState state) -> {
            if (state.isOf(MyBlocks.AIR_CONDITIONER)) {
                return ExampleACController.compute(world, pos);
            }
            return null;
        }, 16 /* maxRangeHint */);
    }
}

Choosing baseline: raw outdoor vs. effective vs. full ambient

  • raw outdoor (getEnvironmentCelsius) = biome + season + diurnal; excludes indoor lag and block sources
    • Best for actuators that target a predictable “background” climate and avoid self-induced feedback loops.
  • effective environment (getEffectiveEnvironmentCelsius) = the above after indoor lag; excludes block sources
    • Best for HUDs/logic that should reflect the lagged indoor air without block hotspots.
  • full ambient (getTemperatureCelsius) = biome + season + diurnal + indoor lag + block sources
    • The "what an entity actually feels" value. Use this for body-temperature/gameplay reads.

HUD/Overlay example (raw vs. effective vs. full)

import gavinx.temperatureapi.api.TemperatureAPI;
import net.minecraft.entity.player.PlayerEntity;

public class ExampleHUD {
    public static String outdoorHUD(PlayerEntity player) {
        double envC = TemperatureAPI.getEnvironmentCelsius(player);
        return String.format("Outdoor: %.1f°C", envC);
    }

    public static String indoorHUD(PlayerEntity player) {
        double effC = TemperatureAPI.getEffectiveEnvironmentCelsius(player);
        return String.format("Indoor: %.1f°C", effC);
    }

    public static String ambientHUD(PlayerEntity player) {
        double ambC = TemperatureAPI.getTemperatureCelsius(player);
        return String.format("Ambient: %.1f°C", ambC);
    }
}

Notes and tips

  • Null handling:
    • Numeric methods return NaN if inputs are null; check with Double.isNaN(value).
    • Formatted methods return "N/A" when inputs are invalid.
  • Seasonal/diurnal offsets:
    • If those systems are not present or inactive, the offsets will effectively be 0.0.
  • Indoor lag:
    • Only applies in dimensions with skylight (not Nether/End), and only when the queried position is inside an enclosed space.
  • Units:
    • For formatted strings: TemperatureAPI.getTemperature(world, pos, Unit.CELSIUS/FAHRENHEIT) or pass "F"/"C".
  • Performance:
    • Temperature lookups internally cache block-based effects roughly 80 ticks.
    • Room scans are cached per structure (~100 ticks); per-query indoor math is O(1).

HumidityAPI

What it gives you:

  • A formatted string like “40%”, or
  • A numeric int 0..100

Methods:

  • String getHumidity(World world, BlockPos pos)
  • String getHumidity(PlayerEntity player)
  • int getHumidityValue(World world, BlockPos pos)
  • int getHumidityValue(PlayerEntity player)

Example:

package com.example.yourmod;

import gavinx.temperatureapi.api.HumidityAPI;
import net.minecraft.entity.player.PlayerEntity;

public final class UseHumidityAPI {
    public static String hudLine(PlayerEntity player) {
        return "Humidity: " + HumidityAPI.getHumidity(player); // e.g., "55%"
    }
    public static int numericHumidity(PlayerEntity player) {
        return HumidityAPI.getHumidityValue(player); // 0..100
    }
}

BiomeAPI

Purpose:

  • Provide (or read) custom per-biome temperature (Celsius) and humidity (0..100).
  • API automatically loads your contributions from a JSON resource file and merges them with user config.

Important: You do NOT need any Java code to register your biomes.json. Just place it in the correct resource path. You can check if it was successfully loaded in logs, and can check if the properties took correctly via /temperatureapi biome

File path in your mod:

  • src/main/resources/assets/[MODNAME]/temperatureapi/biomes.json

Schema:

  • "biome" must be "biome.namespace.path" (note the leading biome. prefix).
  • "temperature" is in Celsius (double).
  • "humidity" is 0..100 (int).

Example file:

[
  { "biome": "biome.minecraft.plains",        "temperature": 22.0, "humidity": 40 },
  { "biome": "biome.minecraft.desert",        "temperature": 38.0, "humidity": 5  },
  { "biome": "biome.yourmod.crystal_caves", "temperature": 4.0,  "humidity": 80 }
]

BodyTemperatureAPI

Purpose:

  • Compute passive body temperature rate-of-change (°C/s) from ambient temperature, humidity, player resistances, and soaked status.
  • Provide simple integration helpers to advance a stored body temperature.

Comfort model (defaults):

  • Comfort band: 13.0°C .. 30.0°C
  • Below comfort: cools down; the colder it is, the faster it cools.
  • Above comfort: heats up; the hotter it is, the faster it heats (extra boost if humidity > 50%).
  • Within comfort: relax gently toward normal (36.6°C).

Wetness (Soaked) integration:

  • When soaked:
    • In cold ambient (below comfort), cooling accelerates (1.8x).
    • In hot ambient (above comfort), heating is reduced (0.6x).
  • Soaked is automatically considered in PlayerEntity overloads.

Indoor lag note:

  • Because body-temperature reads use the full ambient (getTemperatureCelsius), they automatically benefit from indoor insulation — sheltering in a sealed, insulated room slows how fast the outdoors drives your body temperature.

Methods (selection):

  • double computeRateCPerSecond(World world, BlockPos pos)
  • double computeRateCPerSecond(PlayerEntity player)
  • double computeRateCPerSecond(PlayerEntity player, double currentBodyTempC)
  • double computeRateCPerSecond(double ambientC, int humidityPercent)
  • double advanceBodyTemp(World world, BlockPos pos, double currentBodyTempC, double dtSeconds)
  • double advanceBodyTemp(PlayerEntity player, double currentBodyTempC, double dtSeconds)

Example: compute passive rate and advance a custom temp value

package com.example.yourmod;

import gavinx.temperatureapi.api.BodyTemperatureAPI;
import net.minecraft.entity.player.PlayerEntity;

public final class UseBodyTemperatureAPI {
    // For mods that track their own body temp (if not using the builtin state)
    public static double integrate(PlayerEntity player, double currentC) {
        double dt = 1.0 / 20.0; // per tick
        return BodyTemperatureAPI.advanceBodyTemp(player, currentC, dt);
    }
}

Tip:

  • The API includes a simple internal store (BodyTemperatureState) and keeps it up to date server-side. If you have your own character system, you can ignore that and just call the API methods.

SoakedAPI

What it gives you:

  • A simple per-player “soaked” timer (in seconds), refreshed when:
    • A) The player is touching or submerged in water.
    • B) The player is under open sky while it’s precipitating (rain or snow).
  • Default duration: 10 seconds (refreshed to at least this value on trigger).
  • BodyTemperatureAPI uses soaked to adjust passive body temperature (freeze faster in cold, cool down faster in heat).

Server-authoritative:

  • On the logical client, isSoaked returns false and getters return 0. Use server context to query or modify.

Methods:

  • boolean isSoaked(PlayerEntity player) // server only (client returns false)
  • double getSoakedSeconds(PlayerEntity player) // server only (client returns 0)
  • void setSoakedSeconds(PlayerEntity player, double seconds) // clamp >= 0
  • void addSoakedSeconds(PlayerEntity player, double addSeconds)

Examples:

  • HUD snippet (server-only context)
package com.example.yourmod;

import gavinx.temperatureapi.api.SoakedAPI;
import net.minecraft.server.network.ServerPlayerEntity;

public final class UseSoakedAPI {
    public static String status(ServerPlayerEntity player) {
        return SoakedAPI.isSoaked(player)
            ? "Soaked (" + String.format("%.1f", SoakedAPI.getSoakedSeconds(player)) + "s)"
            : "Dry";
    }
}
  • Extend soaked duration from your own event (e.g., a splash mechanic)
package com.example.yourmod;

import gavinx.temperatureapi.api.SoakedAPI;
import net.minecraft.server.network.ServerPlayerEntity;

public final class AddWetness {
    public static void onSplash(ServerPlayerEntity player) {
        // Add 3 extra seconds on top of whatever is left
        SoakedAPI.addSoakedSeconds(player, 3.0);
    }
}

TemperatureResistanceAPI

Purpose:

  • Compute a player’s heat/cold comfort expansion (in °C) from equipment plus optional providers.

Expands comfort band:

  • heatC increases COMFORT MAX
  • coldC lowers COMFORT MIN (more cold-tolerant)

Built-in item NBT (optional):

  • Single key: "tempapi_resistance"
  • Value format: "heat:" and/or "cold:"
    • Example values:
      • "heat:3"
      • "cold:2"
      • "heat:4,cold:1"
  • Accepted tiers: 1..6
  • Each tier = +2°C (per direction)
  • Keys are case-insensitive ("heat" and "cold"), and both directions can be present on the same item
  • Multiple entries may be separated by commas, semicolons, or whitespace
  • Legacy keys are removed: do not use "tempapi_heat_tier" or "tempapi_cold_tier"

Tier scaling:

  • Tier 1 → 2°C, 2 → 4°C, 3 → 6°C, 4 → 8°C, 5 → 10°C, 6 → 12°C

Clamps:

  • Totals are clamped to 48°C for heat and 48°C for cold by default

Methods:

  • TemperatureResistanceAPI.Resistance computeTotal(PlayerEntity player)
    • fields: double heatC, double coldC
  • void registerProvider(ResistanceProvider provider)
    • provider: Resistance get(PlayerEntity player)
  • double tierToDegrees(int tier)
    • Returns tier * 2.0 for 1..6, else 0
  • Resistance stackResistance(ItemStack stack)
    • Parses "tempapi_resistance" on the given item and returns both directions

Example: show resistance summary

package com.example.yourmod;

import gavinx.temperatureapi.api.TemperatureResistanceAPI;
import net.minecraft.entity.player.PlayerEntity;

public final class UseResistanceAPI {
    public static String describe(PlayerEntity player) {
        TemperatureResistanceAPI.Resistance r = TemperatureResistanceAPI.computeTotal(player);
        return "Resist: +" + r.heatC + "°C heat, +" + r.coldC + "°C cold";
    }
}

Example: registering a provider (optional)

package com.example.yourmod;

import gavinx.temperatureapi.api.TemperatureResistanceAPI;
import net.minecraft.entity.player.PlayerEntity;

public final class ResistanceProviderInit {
    public static void init() {
        TemperatureResistanceAPI.registerProvider(new TemperatureResistanceAPI.ResistanceProvider() {
            @Override
            public TemperatureResistanceAPI.Resistance get(PlayerEntity player) {
                double extraHeat = hasLavaCharm(player) ? 4.0 : 0.0; // +4°C heat (tier 2)
                double extraCold = hasFrostCharm(player) ? 2.0 : 0.0; // +2°C cold (tier 1)
                return new TemperatureResistanceAPI.Resistance(extraHeat, extraCold);
            }
            private boolean hasLavaCharm(PlayerEntity p) { return false; }
            private boolean hasFrostCharm(PlayerEntity p) { return false; }
        });
    }
}

Note:

  • Resistance beyond tier 3 (≈6°C) may be overkill unless your biome has extreme temperatures. Tiers up to 6 (12°C) are supported for mods that need it.

BlockThermalAPI

Overview

  • Purpose: Let mods declare blocks that warm or cool nearby positions.
  • Units:
    • deltaC: degrees Celsius (+ warms, - cools)
    • range: full-strength radius in blocks
    • dropoff: optional per-source attenuation beyond range (cosine curve), capped at 15 blocks
  • Occlusion:
    • FLOOD_FILL (default): flows through passable spaces; shortest-path steps used for distance
    • LINE_OF_SIGHT: straight raycast; Euclidean distance used
  • Directional emission:
    • Optional single face (north, south, west, east, up, down) limits emission to one side of the block
    • If not set, emission is omnidirectional (default)
  • Defaults for new ThermalSource:
    • Occlusion: FLOOD_FILL
    • Dropoff: enabled
    • Dropoff length: 7 blocks (clamped to 15)
    • Curve: cosine
  • Relationship to indoor climate:
    • BlockThermalAPI sources are local hotspots added on top of the (possibly lagged) ambient. They are not lagged by insulation. For blocks that should hold a whole room at a temperature, use BlockSetpointAPI instead.

How attenuation works

  • Full strength within range
  • Beyond range, if dropoffBlocks > 0:
    • t = distanceOverDropoff / dropoffBlocks, clamped to [0..1]
    • weight = cos(t * PI/2)
    • contribution = deltaC * weight
  • FLOOD_FILL uses shortest passable path steps; LOS uses Euclidean distance

Outdoor exposure

  • Why: Helpful to nerf or scale heaters/coolers when a source is “outside,” where heat/cold dissipates more easily.
  • Methods:
    • boolean BlockThermalAPI.isOutside(World world, BlockPos pos)
      • True if there exists a passable path (same rules as FLOOD_FILL) from the source or an adjacent passable cell to an open sky column.
      • Returns false in dimensions without skylight (e.g., Nether/End).
    • int BlockThermalAPI.stepsToOutside(World world, BlockPos pos, int budget)
      • Minimum passable-steps distance to the outdoors (open sky); returns -1 if none found within budget.
      • Use a budget like 24–64. Higher budgets cost more.
    • double BlockThermalAPI.outdoorExposure(World world, BlockPos pos, int budget)
      • Normalized factor in [0,1]: 1.0 means directly outdoors (0 steps), 0.0 means no outdoor path within budget.
      • Useful as a multiplier to scale intensity/range based on outdoor/indoor context.
  • Direction-aware variants (for directional sources):
    • boolean BlockThermalAPI.isOutside(World world, BlockPos pos, Direction face)
    • int BlockThermalAPI.stepsToOutside(World world, BlockPos pos, int budget, Direction face)
    • double BlockThermalAPI.outdoorExposure(World world, BlockPos pos, int budget, Direction face)
    • These seed the search through the given face and constrain traversal to that face’s half-space, allowing lateral openings in front but preventing “wrap-around” behind the source.
  • Consistency with occlusion:
    • Passability matches FLOOD_FILL rules: air, non-colliding shapes, and open doors/trapdoors/fence gates are passable.
    • Blocks with collision shapes (e.g., glass, leaves) are not passable; a solid/glass roof is indoors.
  • Performance:
    • Results cached per world+pos(+budget[+face]) for ~80 ticks.
    • Early-out: returns false/-1/0.0 if the dimension has no skylight.

Example: nerf a directional heater/AC only when outside ahead of its face

import gavinx.temperatureapi.api.BlockThermalAPI;
import net.minecraft.block.BlockState;
import net.minecraft.state.property.Properties;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;

public class ExampleDirectionalOutside {
    public static void register() {
        // Provide a hint that covers your max (range + dropoff)
        BlockThermalAPI.register((World world, BlockPos pos, BlockState state) -> {
            if (!state.isOf(MyBlocks.WALL_AC)) return null;

            // Read the emitting face from BlockState (example uses HORIZONTAL_FACING)
            Direction face = state.get(Properties.HORIZONTAL_FACING);

            // Direction-aware outdoor exposure in [0,1]; budget 32 is a good default
            double exposure = BlockThermalAPI.outdoorExposure(world, pos, 32, face);

            // Base stats
            double baseDelta = -8.0;
            int baseRange = 8;
            int dropoff = 7;

            // Scale down when exposed: up to -25% intensity and -40% range outside
            double scaledDelta = baseDelta * (1.0 - 0.25 * exposure);
            int scaledRange = Math.max(0, (int)Math.round(baseRange * (1.0 - 0.40 * exposure)));

            if (scaledDelta == 0.0 || scaledRange == 0) return null;

            return new BlockThermalAPI.ThermalSource(
                scaledDelta, scaledRange,
                BlockThermalAPI.OcclusionMode.FLOOD_FILL,
                dropoff, BlockThermalAPI.FalloffCurve.COSINE,
                face // directional emission
            );
        }, 24 /* maxRangeHint: include range + dropoff */);
    }
}

Example: nerf a heater when outside (omnidirectional)

import gavinx.temperatureapi.api.BlockThermalAPI;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public class ExampleNerfOutside {
    public static void register() {
        // Provide a generous hint that includes (range + max dropoff)
        BlockThermalAPI.register((World world, BlockPos pos, BlockState state) -> {
            // Baseline heater stats
            double baseDelta = 6.0;
            int baseRange = 7;
            int dropoff = 7;

            // Outdoor exposure in [0,1]; budget 32 is a good default
            double exposure = BlockThermalAPI.outdoorExposure(world, pos, 32);

            // Scale down when exposed: up to -25% intensity and -40% range outside
            double scaledDelta = baseDelta * (1.0 - 0.25 * exposure);
            int scaledRange = Math.max(0, (int)Math.round(baseRange * (1.0 - 0.40 * exposure)));

            if (scaledDelta == 0.0 || scaledRange == 0) return null;

            return new BlockThermalAPI.ThermalSource(
                scaledDelta, scaledRange,
                BlockThermalAPI.OcclusionMode.FLOOD_FILL,
                dropoff, BlockThermalAPI.FalloffCurve.COSINE
            );
        }, 32 /* maxRangeHint: include range + dropoff */);
    }
}

Registering constant sources Default behavior (FLOOD_FILL + cosine dropoff(7))

import gavinx.temperatureapi.api.BlockThermalAPI;

public class ExampleDefaults {
    public static void register() {
        // Heater: +5°C full strength up to 5, cosine dropoff over 7, FLOOD_FILL occlusion
        BlockThermalAPI.register(MyBlocks.HEATER, +5.0, 5);
        // Air Conditioner: -6°C full strength up to 4, cosine dropoff over 7, FLOOD_FILL occlusion
        BlockThermalAPI.register(MyBlocks.AC, -6.0, 4);
    }
}

Static source (no dropoff)

import gavinx.temperatureapi.api.BlockThermalAPI;

public class ExampleStatic {
    public static void register() {
        // Static within range: dropoffBlocks = 0
        BlockThermalAPI.register(
            MyBlocks.STATIC_HEATER, +3.0, 6,
            BlockThermalAPI.OcclusionMode.FLOOD_FILL, 0
        );
    }
}

Custom dropoff length and occlusion

import gavinx.temperatureapi.api.BlockThermalAPI;

public class ExampleCustomDropoff {
    public static void register() {
        // Lamp: +2.5°C up to 6; LOS occlusion; cosine dropoff over 12 (clamped to <= 15)
        BlockThermalAPI.register(
            MyBlocks.LAMP, +2.5, 6,
            BlockThermalAPI.OcclusionMode.LINE_OF_SIGHT, 12
        );
    }
}

Directional emission (single face)

import gavinx.temperatureapi.api.BlockThermalAPI;
import net.minecraft.util.math.Direction;

public class ExampleDirectional {
    public static void register() {
        // AC unit that only cools out its NORTH face, using LOS occlusion and default dropoff(7)
        BlockThermalAPI.register(
            MyBlocks.WALL_AC, -8.0, 8,
            BlockThermalAPI.OcclusionMode.LINE_OF_SIGHT, Direction.NORTH
        );
        // Heater that only heats UP (e.g., a floor vent)
        BlockThermalAPI.register(
            MyBlocks.FLOOR_VENT, +4.0, 5,
            BlockThermalAPI.OcclusionMode.FLOOD_FILL, 7, Direction.UP
        );
    }
}

Directional via dynamic provider (read face from BlockState)

import gavinx.temperatureapi.api.BlockThermalAPI;
import net.minecraft.block.BlockState;
import net.minecraft.state.property.Properties;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;

public class ExampleDirectionalProvider {
    public static void register() {
        // Include max range hint = max(range + dropoff) you might return
        BlockThermalAPI.register((World world, BlockPos pos, BlockState state) -> {
            if (state.isOf(MyBlocks.SMART_AC)) {
                // Get the block's horizontal facing; emit from that face
                Direction face = state.get(Properties.HORIZONTAL_FACING);
                boolean turbo = state.get(Properties.POWERED);
                double deltaC = turbo ? -10.0 : -6.0;
                int range = turbo ? 9 : 6;
                int dropoff = turbo ? 10 : 7;
                return new BlockThermalAPI.ThermalSource(
                    deltaC, range,
                    BlockThermalAPI.OcclusionMode.LINE_OF_SIGHT,
                    dropoff, BlockThermalAPI.FalloffCurve.COSINE,
                    face
                );
            }
            return null;
        }, 20 /* maxRangeHint */);
    }
}

Dynamic providers (compute per block state)

  • Use a provider when your source depends on BlockState, power, fluids, etc.
  • Important: maxRangeHint should include your possible (range + dropoff) so the scanner searches far enough.
import gavinx.temperatureapi.api.BlockThermalAPI;
import net.minecraft.block.BlockState;
import net.minecraft.state.property.Properties;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public class ExampleProvider {
    public static void register() {
        // Provide a generous hint that includes range + max dropoff
        BlockThermalAPI.register((World world, BlockPos pos, BlockState state) -> {
            if (state.isOf(MyBlocks.SMART_HEATER)) {
                boolean powered = state.get(Properties.POWERED);
                double deltaC = powered ? 7.0 : 3.0;
                int range = powered ? 6 : 3;
                int dropoff = powered ? 10 : 5; // clamped to 15
                return new BlockThermalAPI.ThermalSource(
                    deltaC, range,
                    BlockThermalAPI.OcclusionMode.FLOOD_FILL,
                    dropoff, BlockThermalAPI.FalloffCurve.COSINE
                );
            }
            return null;
        }, 16 /* maxRangeHint: include range + potential dropoff */);
    }
}

Minimal static helper

import gavinx.temperatureapi.api.BlockThermalAPI;

public class ExampleStaticHelper {
    public static void register() {
        // Convenience: static source (no dropoff), FLOOD_FILL occlusion
        var ts = BlockThermalAPI.ThermalSource.staticSource(+4.0, 5);
        BlockThermalAPI.register(MyBlocks.RADIATOR, ts.deltaC, ts.range, ts.occlusion, ts.dropoffBlocks);
    }
}

Best practices

  • Choose occlusion to match the source:
    • FLOOD_FILL for things that “flow” through openings (default)
    • LINE_OF_SIGHT for direct radiant sources
  • Directional emission:
    • Use the Direction parameter to emit through a single face (e.g., AC blowing north)
    • Useful for blocks mounted in walls so they don’t cool both inside and outside
    • Does not account for block placement direction by itself. If you pass a fixed direction (e.g., NORTH), it will always emit NORTH. Use a dynamic provider to read the BlockState facing and supply the proper face.
  • Outdoor scaling:
    • For omnidirectional sources: use isOutside/stepsToOutside/outdoorExposure
    • For directional sources: use the face-aware overloads to ensure only openings in front of the emitting face count
    • Use a budget like 32 for a good balance of accuracy and performance
  • Pick dropoff per source:
    • 0 for static; otherwise 1–15 for cosine falloff distance
    • Negative deltaC works the same as positive (cooling vs heating)
    • Dynamic providers: set maxRangeHint to the max of (range + dropoff) you’ll ever return
  • Performance:
    • Results cached per (world, pos) for about 80 ticks
    • Flood-fill shortest paths cached per source/budget for about 80 ticks
    • Outdoor exposure results cached per world+pos+budget (and face for face-aware) for about 80 ticks

API reference (essentials)

  • Register constant source (omnidirectional):
    • register(Block block, double deltaC, int range)
    • register(Block block, double deltaC, int range, OcclusionMode occlusion)
    • register(Block block, double deltaC, int range, OcclusionMode occlusion, int dropoffBlocks)
  • Register constant source (directional, single face):
    • register(Block block, double deltaC, int range, Direction face)
    • register(Block block, double deltaC, int range, OcclusionMode occlusion, Direction face)
    • register(Block block, double deltaC, int range, OcclusionMode occlusion, int dropoffBlocks, Direction face)
  • Dynamic provider:
    • register(BlockThermalAPI.Provider provider, int maxRangeHint)
    • Provider returns new ThermalSource(deltaC, range, occlusion, dropoffBlocks, FalloffCurve.COSINE[, Direction face])
  • Outdoor exposure:
    • boolean isOutside(World world, BlockPos pos)
    • int stepsToOutside(World world, BlockPos pos, int budget)
    • double outdoorExposure(World world, BlockPos pos, int budget)
    • boolean isOutside(World world, BlockPos pos, Direction face)
    • int stepsToOutside(World world, BlockPos pos, int budget, Direction face)
    • double outdoorExposure(World world, BlockPos pos, int budget, Direction face)
  • Occlusion modes: LINE_OF_SIGHT, FLOOD_FILL
  • Falloff curve: COSINE (current)

Notes

  • dropoffBlocks is clamped to 15; larger values are treated as 15
  • influence radius = range + dropoffBlocks; used for searching and occlusion budgets
  • Directional emission uses the face’s outward normal; only targets in that half-space are considered
  • Outdoor exposure (face-aware) seeds through the face and remains in that face’s half-space to avoid counting outside behind the source
  • If Direction is not provided, emission is omnidirectional by default

Indoor Climate System (New)

Overview

  • Enclosed structures resist outdoor temperature changes. Instead of the indoor air tracking the outside instantly, it lags behind based on how well-insulated and how enclosed the space is.
  • Only the ambient term (biome + season + diurnal) is lagged. Block thermal sources (BlockThermalAPI), resistances, soaked, etc. are unaffected and apply on top.
  • Four pieces work together:
    • BlockInsulationAPI — declares each block's insulation (0.0–1.0; default 0.3).
    • RoomAPI — detects the enclosed space and summarizes its enclosure + average insulation.
    • BlockSetpointAPI — thermostatic emitters that drive a room toward a target temperature.
    • IndoorClimateAPI — the model that lags ambient and applies emitters; wired into TemperatureAPI automatically.
  • Zero-config: every block defaults to 0.3 insulation, so structures insulate sensibly out of the box. You only register values to tune specific blocks, and BlockSetpointAPI only if you add thermostats.
  • Dimensions without skylight (Nether/End) are excluded entirely.

The model (how the lag is computed)

  • Each block has an insulation value in [0,1]. A wall block embedded in a contiguous surface is boosted toward 1.0 ("middle of the wall" insulates better than a corner); the boost uses the in-plane neighbors via an isotropic weighting (4·edges + diagonals)/20. This is handled automatically by RoomAPI — modders just register base values.
  • A room's enclosure is wallFaces / (wallFaces + LEAK_WEIGHT · leakFaces) in [0,1], where a leak face is an interior face open to the sky. Sealed room = 1.0; open doorway / roof hole lowers it; outdoors = 0.0; deep cave = ~1.0.
  • Effective insulation alpha = avgInsulation × enclosure.
  • Indoor air relaxes toward the outdoors with a half-life H_SCALE · alpha/(1−alpha): alpha = 0 tracks the outdoors instantly, alpha = 1 never changes.
  • Returning players / reloaded chunks: because the outdoor curve is deterministic (seeded per day), the indoor temperature is reconstructed by replaying the real past outdoor history over a bounded window — accurate to the actual day/time, no save data required.

Debug command

  • /temperatureapi room [pos] prints enclosure %, average insulation, alpha, lag half-life, indoor (lagged) vs outdoor (raw) temperature, room volume, and any thermostatic emitters.

BlockInsulationAPI

Purpose:

  • Declare how much a block resists indoor temperature change, from 0.0 (no resistance) to 1.0 (maximal). Used by the room scanner to compute a structure's insulation.

Defaults:

  • BlockInsulationAPI.DEFAULT_INSULATION = 0.3. Any block without a registered value resolves to 0.3, so vanilla and unconfigured modded blocks still participate.

Effective vs. base insulation:

  • The value you register is the base. A boundary block's effective insulation is boosted toward 1.0 by same-surface neighbors (a block in the middle of a wall/floor/ceiling insulates better than an edge or corner). This boost is computed automatically by RoomAPI; you do not call it yourself.

Methods:

  • void register(Block block, double insulation) // clamped to [0,1]
  • void register(BlockInsulationAPI.Provider provider) // Double get(World, BlockPos, BlockState); return null to abstain
  • double getBaseInsulation(World world, BlockPos pos, BlockState state) // registered, else provider, else 0.3
  • double applyConnectivity(double base, int edges, int diagonals) // the (4·edges + diagonals)/20 boost (used internally)
  • boolean hasAny()

Example: register constant insulation values

package com.example.yourmod;

import gavinx.temperatureapi.api.BlockInsulationAPI;

public final class InsulationInit {
    public static void init() {
        BlockInsulationAPI.register(MyBlocks.INSULATED_PANEL, 0.9); // excellent insulator
        BlockInsulationAPI.register(MyBlocks.LOG_WALL,        0.6);
        BlockInsulationAPI.register(MyBlocks.THIN_GLASS,      0.1); // leaks heat readily
        // Anything not registered uses the 0.3 default.
    }
}

Example: state-dependent insulation via a provider

package com.example.yourmod;

import gavinx.temperatureapi.api.BlockInsulationAPI;
import net.minecraft.block.BlockState;
import net.minecraft.state.property.Properties;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public final class InsulationProvider {
    public static void init() {
        BlockInsulationAPI.register((World world, BlockPos pos, BlockState state) -> {
            if (state.isOf(MyBlocks.SHUTTERED_WINDOW)) {
                // Closed shutters insulate well; open shutters barely at all.
                return state.get(Properties.OPEN) ? 0.1 : 0.8;
            }
            return null; // abstain -> falls through to registry / default
        });
    }
}

BlockSetpointAPI

Purpose:

  • Register thermostatic ("setpoint") emitters: blocks that try to hold the room they are in at a target temperature, rather than adding a fixed offset like BlockThermalAPI.
  • Insulation-aware automatically: in a poorly sealed/insulated room the outdoor pull overwhelms the emitter, so you cannot heat an open field. Better insulation makes the same emitter more effective — exactly like real life.

Key concepts:

  • targetC — the temperature the emitter drives the room toward.
  • Mode:
    • HEAT_ONLY — only adds heat while the room is below target (fireplace, furnace).
    • COOL_ONLY — only removes heat while the room is above target (cooler, AC).
    • BOTH — always drives toward target (full HVAC).
  • responseHalfLifeTicks — time to close ~half the gap to the setpoint in a reference (3×3×3) sealed room. Power is scaled down automatically for larger rooms (more air to drive), so a candle warms a closet fast but barely moves a cathedral.
  • persistsWhileUnloaded — if true, the emitter is treated as running even while its chunk is unloaded (used during reconstruction). Default false: while unloaded the chunk isn't ticking, so the room drifts toward outdoor and the emitter resumes when reloaded.
  • Multiple emitters combine: same-mode emitters reach a shared target faster; a heater and an AC settle to a power-weighted equilibrium between their setpoints.
  • BlockThermalAPI delta sources still stack on top (a roaring hearth can be both a radiant hotspot and a room conditioner if registered in both).

Constants:

  • BlockSetpointAPI.V_REF = 27.0 — reference interior volume (3×3×3) used for power scaling.

Methods:

  • void register(Block block, double targetC, double responseHalfLifeTicks, Mode mode)
  • void register(Block block, double targetC, double responseHalfLifeTicks, Mode mode, boolean persistsWhileUnloaded)
  • void register(BlockSetpointAPI.Provider provider) // Setpoint get(World, BlockPos, BlockState); return null to abstain
  • BlockSetpointAPI.Setpoint resolve(World world, BlockPos pos, BlockState state)
  • double powerPerTick(Setpoint s, int volume)
  • boolean hasAny()

Example: a fireplace, an AC, and an always-on HVAC

package com.example.yourmod;

import gavinx.temperatureapi.api.BlockSetpointAPI;
import gavinx.temperatureapi.api.BlockSetpointAPI.Mode;

public final class SetpointInit {
    public static void init() {
        // Warms its room toward 22C, reaching ~half the gap in 600 ticks (~30s) in a small sealed room.
        BlockSetpointAPI.register(MyBlocks.FIREPLACE, 22.0, 600, Mode.HEAT_ONLY);

        // Cools its room toward 18C; never heats.
        BlockSetpointAPI.register(MyBlocks.AIR_CONDITIONER, 18.0, 500, Mode.COOL_ONLY);

        // Full climate unit that keeps 21C in either direction, even while the chunk is unloaded.
        BlockSetpointAPI.register(MyBlocks.HVAC_UNIT, 21.0, 400, Mode.BOTH, true);
    }
}

Example: a thermostat whose target is configured via BlockState/GUI

package com.example.yourmod;

import gavinx.temperatureapi.api.BlockSetpointAPI;
import gavinx.temperatureapi.api.BlockSetpointAPI.Mode;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public final class ThermostatProvider {
    public static void init() {
        BlockSetpointAPI.register((World world, BlockPos pos, BlockState state) -> {
            if (!state.isOf(MyBlocks.THERMOSTAT)) return null;
            if (!isPowered(world, pos)) return null; // off -> no setpoint
            double target = readTargetFromBlockEntity(world, pos); // e.g., 24.0
            return new BlockSetpointAPI.Setpoint(target, 450, Mode.BOTH, false);
        });
    }

    private static boolean isPowered(World w, BlockPos p) { return true; }
    private static double readTargetFromBlockEntity(World w, BlockPos p) { return 24.0; }
}

IndoorClimateAPI

Purpose:

  • The indoor temperature model. Lags the outdoor ambient inside enclosed structures and applies thermostatic emitters. Wired into TemperatureAPI automatically — most mods never call this directly.

Methods:

  • double adjustAmbient(World world, BlockPos pos, double outdoorEnvC)
    • Given the raw outdoor environment (biome + season + diurnal), returns the effective ambient after indoor lag + emitters. Equals the input when outdoors or in no-skylight dimensions.
  • double passiveRate(double alpha)
    • The per-tick relaxation rate toward the outdoors for an effective insulation alpha. Exposed for inspection/HUDs.

Tunables (current values; edit in source to retune):

  • H_SCALE = 3000 — lag half-life scale in ticks. A sealed default-block house lands around a 7–12 minute half-life depending on size. Lower this for faster indoor swings.
  • ALPHA_FROZEN = 0.9995 — at/above this alpha the room is treated as perfectly frozen (no change).
  • K_MAX = 10.0 — cap on the lag rate for near-zero alpha (snaps to outdoors).
  • TICKS_PER_STEP = 1200 — reconstruction step (1 in-game minute), matching the diurnal quantization.
  • RECON_HALFLIVES = 5 — trailing reconstruction window in half-lives.
  • RECON_MAX_TICKS = 48000 — hard cap on the reconstruction window (2 in-game days).
  • (Enclosure tuning lives in RoomAPI: LEAK_WEIGHT controls how much an open doorway/window bleeds.)

Example: read the lag rate for a HUD

package com.example.yourmod;

import gavinx.temperatureapi.api.IndoorClimateAPI;
import gavinx.temperatureapi.api.RoomAPI;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public final class IndoorHud {
    public static String lagLine(World world, BlockPos pos) {
        RoomAPI.Room room = RoomAPI.get(world, pos);
        if (room == null || room.outdoor) return "Outdoors";
        double k = IndoorClimateAPI.passiveRate(room.effectiveAlpha());
        if (k <= 0.0) return "Sealed (no drift)";
        double halfLifeMin = (Math.log(2.0) / k) / 1200.0;
        return String.format("Insulated: ~%.1f min half-life", halfLifeMin);
    }
}

RoomAPI

Purpose:

  • Detect the enclosed space around a position and summarize it: enclosure, average (effective) insulation, volume, and any thermostatic emitters present. Read-oriented; useful for HUDs, debugging, or custom climate logic.

Performance:

  • Scans are per-structure (one bounded BFS shared by everyone inside) and cached ~100 ticks; a query is O(1) on the common path. Budget: up to 32 steps / 4096 cells. Up to 64 rooms cached per world.

Constants:

  • MAX_STEPS = 32, MAX_CELLS = 4096, ROOM_TTL = 100, MAX_ROOMS = 64
  • LEAK_WEIGHT = 6.0 — how much one sky-venting face leaks relative to a wall face. Raise it to make open doors/windows bleed heat more aggressively.
  • OUTDOOR_EPSILON = 0.02 — enclosure at/below this is treated as outdoors.

Types:

  • RoomAPI.Room (fields):
    • double enclosure // 0..1
    • double avgInsulation // 0..1 (effective, connectivity-boosted)
    • int volume // interior cell count
    • boolean outdoor // true if effectively open to the sky
    • List setpoints
    • boolean hasPersistentSetpoint
    • double effectiveAlpha() // avgInsulation * enclosure (0 outdoors)
  • RoomAPI.ActiveSetpoint (record): double power, double targetC, BlockSetpointAPI.Mode mode, boolean persistent

Methods:

  • RoomAPI.Room get(World world, BlockPos pos) // never null; outdoor positions return a room with outdoor == true
  • void invalidate(World world) // drop cached rooms for a world

Example: room summary line

package com.example.yourmod;

import gavinx.temperatureapi.api.RoomAPI;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

public final class RoomReadout {
    public static String describe(World world, BlockPos pos) {
        RoomAPI.Room room = RoomAPI.get(world, pos);
        if (room == null || room.outdoor) return "Outdoors";
        return String.format("Enclosure %.0f%%, insulation %.2f, %d cells%s",
            room.enclosure * 100.0,
            room.avgInsulation,
            room.volume,
            room.setpoints.isEmpty() ? "" : ", " + room.setpoints.size() + " emitter(s)");
    }
}

Notes

  • Units: TemperatureAPI supports Celsius and Fahrenheit; use the Unit enum, boolean flag, or unit string ("C"/"F").
  • Seasonal integration: If Serene Seasons is present, TemperatureAPI includes a seasonal offset. If not present, the offset is 0.
  • Day/night integration: TemperatureAPI includes a smooth diurnal offset (quantized per in-game minute) and keeps client displays synced.
  • Indoor climate: The full ambient lags inside enclosed, insulated structures (ambient term only — block sources are applied on top). It is automatic via getTemperatureCelsius, works with the 0.3 default for all blocks, and is reconstructed accurately when chunks reload. Only active in dimensions with skylight.
  • Body temp + soaked: Player-based BodyTemperatureAPI calls automatically account for resistances, soaked status, and indoor lag on the server.
  • Debug: /temperatureapi room [pos] reports the room's enclosure, insulation, alpha, lag half-life, indoor vs. outdoor temperature, volume, and emitters.

Troubleshooting

  • “My biomes.json isn’t being used”:
    • Confirm the resource path is exactly src/main/resources/temperatureapi/biomes.json.
    • Ensure the biome keys start with biome. and match your registry ids (e.g., biome.yourmodid.some_biome).
  • “Seasonal offset is always 0”:
    • That’s expected unless Serene Seasons is installed and detected.
  • “Soaked is always false on client”:
    • SoakedAPI is server-authoritative. Query or modify from server-side context; client-side returns false/0 by design.
  • “Standing under glass while raining doesn’t soak me”:
    • Correct. The system checks for open sky (world.isSkyVisible above the player) to avoid indoor false positives.
  • “The indoor temperature doesn’t change instantly when it gets cold/hot outside”:
    • Expected — that's the insulation lag. Better insulation / enclosure means a longer half-life. Use /temperatureapi room to see it; lower H_SCALE (IndoorClimateAPI) for faster swings.
  • “My heater/AC won’t hold a temperature in an open area”:
    • Expected — setpoint emitters are insulation-aware. In a poorly sealed or uninsulated space the outdoor pull overwhelms them. Seal and insulate the room, or raise the emitter power (lower its response half-life).
  • “Opening a door doesn’t change the temperature fast enough”:
    • Opening a door lowers enclosure (it becomes a leak), which speeds indoor/outdoor exchange — but a room rescans at most every ~100 ticks (~5s), so there's a brief delay. To make open doors/windows bleed harder, raise LEAK_WEIGHT (RoomAPI).
  • “I came back to my base and it had gone cold”:
    • Expected for non-persistent emitters: while a chunk is unloaded it isn't ticking, so the fire effectively went out and the room drifted toward outdoor; it re-warms once reloaded. Register the emitter with persistsWhileUnloaded = true for always-on climate control.
  • “No indoor lag in the Nether/End”:
    • Correct. The indoor system only runs in dimensions with skylight.