Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions .agent/tasks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,14 @@ Bug-ledger history lives in
2026-05-30) minus #2 (dropped 2026-05-31 as impl-trivia — see entry)
minus the 2026-06-01/02 fixed batch — #8 (weight-rework) / #9 (mod-container,
PR #22) / #10 (planetDefs tolerance, PR #22) / #11 (JEI guard, PR #22) /
#12 (TASK-45) / #13 (beds, PR #22) / #14 (railgun #61, TASK-49) = 4 live
#12 (TASK-45) / #13 (beds, PR #22) / #14 (railgun #61, TASK-49) /
#15 (canDoRainSnowIce density-scale, feature/better_weather) = 4 live
(#1, #3, #5, #7). Entries #8–#14 were renumbered chronologically when the
feature/postponed and 1.12 ledgers merged (2026-06-14), resolving a #8/#9
collision. Batch #2 opened 2026-05-25; entry #5 added 2026-05-29; entry #7
added 2026-05-31. Batch #1 fully drained by TASK-12 on 2026-05-23. Entries:
collision; #15 was added when feature/better_weather merged into 1.12 (its
own ledger #8 renumbered to avoid colliding). Batch #2 opened 2026-05-25;
entry #5 added 2026-05-29; entry #7 added 2026-05-31. Batch #1 fully drained
by TASK-12 on 2026-05-23. Entries:
(1) `SatelliteRegistry.getNewSatellite` returns `null` for unknown
types instead of the documented `SatelliteDefunct` fallback —
pinned by `SatelliteRegistryFallbackTest._documentsKnownBug` pair.
Expand Down Expand Up @@ -364,6 +367,20 @@ Bug-ledger history lives in
silent unloaded-dest characterization) via the new `infra railgun-fire`
probe. Fix (load dest dim on fire + per-cause feedback) tracked by
TASK-49. Found 2026-06-02 during #61 investigation.
(15) ✅ **FIXED 2026-06-01 in `feature/better_weather`.**
`WorldProviderPlanet.canDoRainSnowIce` compared
`getAtmosphereDensity(pos)` — which returns the density already
divided by 100 (range ~0..2) — against the literal `75`, so the
predicate was effectively always false. Player-visible: AR planets
never accumulated rain/snow/ice through this gate regardless of
atmosphere. Fixed while wiring the new `minAtmosphereDensityForRain`
config threshold: the check now compares the raw 0..100
`props.getAtmosphereDensity()` against the configurable threshold
(default 75), matching the scale used everywhere else
(XML `atmosphereDensity`, `AtmosphereTypes`). Found during the
feature/better_weather atmosphere-gate work. Does NOT change the live
count (found and fixed in the same change). Renumbered from its
original ledger #8 on the feature/better_weather → 1.12 merge.

## Done

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,8 @@ public void postInit(FMLPostInitializationEvent event) {
MinecraftForge.EVENT_BUS.register(new EntityEventHandler());
// Async weather info injection
MinecraftForge.EVENT_BUS.register(new zmaster587.advancedRocketry.world.weather.PlanetWeatherEventHandler());
// Acid rain damage on planets flagged acidicRain
MinecraftForge.EVENT_BUS.register(new zmaster587.advancedRocketry.event.AcidRainHandler());

WirelessDataTickHandler wirelessTickHandler = new WirelessDataTickHandler();
MinecraftForge.EVENT_BUS.register(wirelessTickHandler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ public class ARConfiguration {
public boolean logPlanetWeatherWrapping = true;
@ConfigProperty
public boolean forcePlanetWeatherWorldInfoWrapper = false;
@ConfigProperty(needsSync = true)
public float minAtmosphereDensityForRain = 75f;
@ConfigProperty
public float acidRainDamage = 1f;
@ConfigProperty
public int acidRainDamageInterval = 20;
@ConfigProperty
public float spaceLaserPowerMult;
@ConfigProperty
Expand Down Expand Up @@ -487,6 +493,9 @@ public static void loadPreInit() {
arConfig.enableCustomPlanetWeather = config.get(PLANET, "enableCustomPlanetWeather", true, "Sub-toggle of perDimWorldInfo (no effect when that is false): if true, each AR planet has its own weather state (rain, thunder, /weather, isRaining); if false, weather delegates to the overworld while per-dimension time-of-day still applies.").getBoolean();
arConfig.logPlanetWeatherWrapping = config.get(PLANET, "logPlanetWeatherWrapping", true, "Log an info line every time an AR planet's WorldInfo is wrapped for per-dimension weather. Useful for diagnosing weather-wrapping issues; safe to disable in production.").getBoolean();
arConfig.forcePlanetWeatherWorldInfoWrapper = config.get(PLANET, "forcePlanetWeatherWorldInfoWrapper", false, "Force per-dimension weather wrapping on every secondary (non-overworld) dimension, including non-AR dims of other mods. Compatibility/debug flag — do NOT enable unless you know exactly what you are doing.").getBoolean();
arConfig.minAtmosphereDensityForRain = (float) config.get(PLANET, "minAtmosphereDensityForRain", 75d, "Minimum atmosphere density (0-100 scale, same as planet atmosphereDensity) required for rain/snow and thunder on a planet. Below this, rain is suppressed regardless of the planet's weather markers, and thunder cannot occur. Thin/airless worlds stay clear.", 0d, 200d).getDouble();
arConfig.acidRainDamage = (float) config.get(PLANET, "acidRainDamage", 1d, "Damage dealt to an unprotected player standing under open sky while it rains on a planet whose rain is acidic (acidicRain=true in the planet definition). Set 0 to disable acid-rain damage.", 0d, Float.MAX_VALUE).getDouble();
arConfig.acidRainDamageInterval = config.get(PLANET, "acidRainDamageInterval", 20, "Ticks between successive acid-rain damage applications (20 = once per second).", 1, Integer.MAX_VALUE).getInt();
arConfig.blackListAllVanillaBiomes = config.getBoolean("blackListVanillaBiomes", PLANET, false, "Prevent vanilla biomes from spawning on planets.");
arConfig.maxBiomesPerPlanet = config.get(PLANET, "maxBiomesPerPlanet", 99, "Maximum unique biomes per planet.").getInt();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public PlanetCommand() {
addSubcommand(new PlanetGenerateCommand());
addSubcommand(new PlanetSetCommand());
addSubcommand(new PlanetGetCommand());
addSubcommand(new PlanetWeatherCommand());

addSubcommand(new CommandTreeHelp(this));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package zmaster587.advancedRocketry.command.sub.planet;

import net.minecraft.command.CommandException;
import net.minecraft.command.ICommandSender;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.text.TextComponentString;
import org.apache.commons.lang3.math.NumberUtils;
import zmaster587.advancedRocketry.api.ARConfiguration;
import zmaster587.advancedRocketry.command.sub.ARCommand;
import zmaster587.advancedRocketry.dimension.DimensionManager;
import zmaster587.advancedRocketry.dimension.DimensionProperties;
import zmaster587.advancedRocketry.world.weather.PlanetWeatherManager;
import zmaster587.advancedRocketry.world.weather.PlanetWeatherState;

/**
* {@code /ar planet weather [dimId]} — prints a planet's weather profile (the
* static markers + acidic flag) together with its live state and the tick
* countdown to the next rain/thunder change. The profile values themselves are
* also editable through {@code /ar planet get|set <dim> rainMarker|...}; this
* command exists so a player can read the runtime state, which is not stored on
* {@link DimensionProperties}.
*/
public class PlanetWeatherCommand extends ARCommand {

@Override
public String getName() {
return "weather";
}

@Override
public String getUsage(ICommandSender sender) {
return "commands.advancedrocketry.planet.weather.usage";
}

@Override
public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException {
int dimId = args.length >= 1 && NumberUtils.isParsable(args[0])
? parseInt(args[0])
: sender.getEntityWorld().provider.getDimension();

if (!DimensionManager.getInstance().isDimensionCreated(dimId)) {
throw invalidValue("Dimension", dimId);
}

DimensionProperties props = DimensionManager.getInstance().getDimensionProperties(dimId);

float density = props.getAtmosphereDensity();
float threshold = ARConfiguration.getCurrentConfig().minAtmosphereDensityForRain;
boolean canRain = density >= threshold;

send(sender, "=== Weather: " + props.getName() + " (dim " + dimId + ") ===");
send(sender, "Profile: rain=" + markerWord(props.getRainMarker())
+ ", thunder=" + markerWord(props.getThunderMarker())
+ ", acidic=" + (props.isAcidicRain() ? "yes" : "no"));
send(sender, "Atmosphere: density " + (int) density + "/100 — rain "
+ (canRain ? "allowed" : "suppressed") + " (min " + (int) threshold + ")");

PlanetWeatherState state = PlanetWeatherManager.getOrCreate(server, dimId);
if (state == null) {
send(sender, "Now: <state unavailable — overworld not loaded>");
return;
}

String now = state.isThundering() ? "thunderstorm" : (state.isRaining() ? "raining" : "clear");
send(sender, "Now: " + now);
send(sender, "Next change: rain in " + state.getRainTime() + "t, thunder in "
+ state.getThunderTime() + "t");
}

private static String markerWord(int marker) {
if (marker > 0) return "always";
if (marker < 0) return "never";
return "dynamic";
}

private static void send(ICommandSender sender, String line) {
sender.sendMessage(new TextComponentString(line));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraft.world.storage.WorldInfo;
import zmaster587.advancedRocketry.api.ARConfiguration;
import zmaster587.advancedRocketry.command.sub.ARCommand;
import zmaster587.advancedRocketry.dimension.DimensionProperties;
import zmaster587.advancedRocketry.world.provider.WorldProviderPlanet;
Expand Down Expand Up @@ -45,44 +46,46 @@ public void execute(MinecraftServer server, ICommandSender sender, String[] args
DimensionProperties props = ((WorldProviderPlanet) world.provider).getDimensionProperties();
Preconditions.checkNotNull(props);

// A planet whose atmosphere is below the rain threshold can never
// hold precipitation: WorldProviderPlanet.updateWeather() forces it
// clear every tick, so refuse rain/thunder up front instead of
// "succeeding" and being silently reverted on the next tick.
final boolean canRain = props.getAtmosphereDensity()
>= ARConfiguration.getCurrentConfig().minAtmosphereDensityForRain;

WorldInfo worldinfo = world.getWorldInfo();

if ("clear".equalsIgnoreCase(args[0])) {
// Check if clear weather is allowed
if (props.getRainMarker() == 1 || props.getThunderMarker() == 1) {
notifyCommandListener(sender, this, "commands.weather.always_not_clear");
return;
}
String action = args[0].toLowerCase(java.util.Locale.ROOT);
if (!"clear".equals(action) && !"rain".equals(action) && !"thunder".equals(action)) {
throw new WrongUsageException("commands.weather.usage");
}

// Refuse up front anything WorldProviderPlanet.updateWeather() would
// revert next tick (a forcing marker, or — for rain/thunder — an
// atmosphere too thin to hold precipitation), instead of "succeeding"
// and being silently undone.
String refusal = weatherRefusalKey(action, props.getRainMarker(),
props.getThunderMarker(), canRain);
if (refusal != null) {
notifyCommandListener(sender, this, refusal);
return;
}

if ("clear".equals(action)) {
worldinfo.setCleanWeatherTime(i);
worldinfo.setRainTime(0);
worldinfo.setThunderTime(0);
worldinfo.setRaining(false);
worldinfo.setThundering(false);
notifyCommandListener(sender, this, "commands.weather.clear");
} else if ("rain".equalsIgnoreCase(args[0])) {
// Check if raining is allowed
if (props.getRainMarker() == -1) {
notifyCommandListener(sender, this, "commands.weather.cannot_rain");
return;
}

} else if ("rain".equals(action)) {
worldinfo.setCleanWeatherTime(0);
worldinfo.setRainTime(i);
worldinfo.setThunderTime(i);
worldinfo.setRaining(true);
worldinfo.setThundering(false);
notifyCommandListener(sender, this, "commands.weather.rain");
} else {
if (!"thunder".equalsIgnoreCase(args[0])) {
throw new WrongUsageException("commands.weather.usage");
}
// Check if thunder is allowed
if (props.getThunderMarker() == -1) {
notifyCommandListener(sender, this, "commands.weather.cannot_thunder");
return;
}

} else { // thunder
worldinfo.setCleanWeatherTime(0);
worldinfo.setRainTime(i);
worldinfo.setThunderTime(i);
Expand All @@ -95,6 +98,40 @@ public void execute(MinecraftServer server, ICommandSender sender, String[] args
}
}

/**
* Whether {@code /advancedrocketry weather <action>} can take effect on a
* planet, or would be reverted next tick by
* {@link WorldProviderPlanet#updateWeather()}. Returns the lang key of the
* refusal message, or {@code null} if the action is allowed. Pure function of
* the planet's weather markers + the atmosphere can-rain gate, so it is
* unit-tested directly ({@code WeatherCommandRefusalTest}).
*
* <ul>
* <li><b>clear</b> — refused when a marker forces rain/thunder always-on;</li>
* <li><b>rain</b> — refused when the rain marker is "never" ({@code -1}) or
* the atmosphere is too thin to hold rain;</li>
* <li><b>thunder</b> — refused when the thunder marker is "never", or when
* rain itself is impossible (rain marker "never" or thin atmosphere),
* since {@code updateWeather} clears thunder whenever it is not raining.</li>
* </ul>
*/
public static String weatherRefusalKey(String action, int rainMarker, int thunderMarker, boolean canRain) {
if ("clear".equalsIgnoreCase(action)) {
return (rainMarker == 1 || thunderMarker == 1) ? "commands.weather.always_not_clear" : null;
}
if ("rain".equalsIgnoreCase(action)) {
if (rainMarker == -1) return "commands.weather.cannot_rain";
if (!canRain) return "commands.weather.cannot_rain_atmosphere";
return null;
}
if ("thunder".equalsIgnoreCase(action)) {
if (thunderMarker == -1) return "commands.weather.cannot_thunder";
if (rainMarker == -1 || !canRain) return "commands.weather.cannot_thunder_norain";
return null;
}
return null;
}

public List<String> getTabCompletions(MinecraftServer server, ICommandSender sender, String[] args, @Nullable BlockPos targetPos) {
if (args.length == 1) {
return getListOfStringsMatchingLastWord(args, "clear", "rain", "thunder");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ private static float clampFeatureFrequencyMultiplier(float multiplier) {
private int thunderProlongationLength = WEATHER_PROLONGATION_LENGTH;
private int rainMarker; // -1 - never rain, 1 - always rain, 0 - regular weather
private int thunderMarker; // -1 - never thunder, 1 - always thunder, 0 - regular weather
private boolean acidicRain; // rain on this planet harms unprotected players under open sky

IAtmosphere atmosphereType;
StellarBody star;
Expand Down Expand Up @@ -1690,6 +1691,8 @@ else if (nbt.hasKey("biomes", NBT.TAG_INT_ARRAY)) {
setRainMarker(nbt.getInteger("rainMarker"));
if (nbt.hasKey("thunderMarker", NBT.TAG_INT))
setThunderMarker(nbt.getInteger("thunderMarker"));
if (nbt.hasKey("acidicRain"))
setAcidicRain(nbt.getBoolean("acidicRain"));

// Sanity clamp
if (getRainStartLength() <= 0) setRainStartLength(WEATHER_START_LENGTH);
Expand Down Expand Up @@ -2042,6 +2045,7 @@ public void writeToNBT(NBTTagCompound nbt) {
nbt.setInteger("thunderProlongationLength", getThunderProlongationLength());
nbt.setInteger("rainMarker", getRainMarker());
nbt.setInteger("thunderMarker", getThunderMarker());
nbt.setBoolean("acidicRain", isAcidicRain());

//Hierarchy
if (!childPlanets.isEmpty()) {
Expand Down Expand Up @@ -2382,6 +2386,14 @@ public void setThunderMarker(int marker) {
this.thunderMarker = marker;
updateCustomWorldInfo();
}

public boolean isAcidicRain() {
return acidicRain;
}

public void setAcidicRain(boolean acidicRain) {
this.acidicRain = acidicRain;
}
//</editor-fold>

/**
Expand Down
Loading