Skip to content
32bitx64bit edited this page Sep 28, 2025 · 25 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)
  • 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 + block sources)
    • Component-only values you can combine yourself:
      • Biome base temperature (no adjustments)
      • Seasonal offset
      • Diurnal/day-night offset
      • Environment temperature (biome + season + diurnal; excludes block sources)
  • 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 + 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) // biome + season + diurnal; 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 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 + block sources
        return TemperatureAPI.getTemperatureCelsius(world, pos);
    }
}

Component-only usage

  • Read the biome-only temperature and environment (biome + season + diurnal):
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 environment(World world, BlockPos pos) {
        // biome + season + diurnal; excludes block sources
        return TemperatureAPI.getEnvironmentCelsius(world, pos);
    }
}

Implementing a target-temperature controller (e.g., AC that aims for 25°C)

  • Use environment temperature (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: environment vs. full ambient

  • environment = biome + season + diurnal; excludes block sources
    • Best when you want your system to target a predictable “background” climate and avoid self-induced feedback loops.
  • full ambient = biome + season + diurnal + block sources
    • Use this if you want the controller to compensate for other heaters/coolers nearby (including itself), but be careful to avoid oscillation or wind-up.

HUD/Overlay example (environment vs. full)

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

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

    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.
  • 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.
    • Flood-fill occlusion distances for block sources are also cached.

Related: BlockThermalAPI at a glance

  • Register constant or dynamic sources that heat/cool areas
  • Per-source occlusion (FLOOD_FILL by default) and cosine dropoff distance (0–15; 0 = static)
  • Flood-fill uses shortest passable path with a budget of range + dropoff; LOS uses Euclidean distance
  • See the BlockThermalAPI wiki page for detailed examples and best practices

If you want this split into multiple pages (e.g., “Reading Temperatures” and “Controllers/Actuators”), I can provide that structure too.

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.

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

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 (new)

  • 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);
    }
}

Reading temperatures (includes blocks + seasons + diurnal)

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

public class ExampleRead {
    public static String at(World world, BlockPos pos) {
        return TemperatureAPI.getTemperature(world, pos, TemperatureAPI.Unit.CELSIUS); // "20.0°C"
    }
    public static double atC(World world, BlockPos pos) {
        return TemperatureAPI.getTemperatureCelsius(world, pos);
    }
    public static String forPlayer(net.minecraft.entity.player.PlayerEntity player) {
        return TemperatureAPI.getTemperature(player, "F"); // "68.0°F"
    }
}

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

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.
  • Body temp + soaked: Player-based BodyTemperatureAPI calls automatically account for resistances and soaked status on the server.

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.