From 3cf1a2698caaef12ce95e27e66633491802346df Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 10 Aug 2026 20:46:24 +0300 Subject: [PATCH 01/42] refactor: name the astronomical constants by meaning, not by value - three unrelated quantities shared the literal 100 in one file - distance scale, atmosphere scale and star-temperature scale now distinct - year, lunar month, ticks-per-day and the star-radius scale named - default rotational period named separately from the platform tick rate - constants are int with explicit casts so each site keeps its own arithmetic - pins the frame by value first; no computed number changes --- .../dimension/DimensionManager.java | 3 +- .../dimension/DimensionProperties.java | 10 ++- .../mixin/MixinWorldServer.java | 3 +- .../util/AstronomicalBodyHelper.java | 65 +++++++++++++---- .../world/weather/ARDimensionWorldInfo.java | 2 +- .../test/unit/AstronomicalBodyHelperTest.java | 71 +++++++++++++++++++ 6 files changed, 138 insertions(+), 16 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java index c0bf30aab..1890f7c68 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java @@ -337,7 +337,8 @@ public DimensionProperties generateRandom(int starId, String name, int baseAtmos properties.ringColor[2] = properties.skyColor[2]; } - properties.rotationalPeriod = (int) (Math.pow((1 / properties.gravitationalMultiplier), 3) * 24000); + properties.rotationalPeriod = (int) (Math.pow((1 / properties.gravitationalMultiplier), 3) + * DimensionProperties.DEFAULT_ROTATIONAL_PERIOD); properties.addBiomes(properties.getViableBiomes(true)); properties.initDefaultAttributes(); diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java index 77cfee5b7..32403b53f 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java @@ -71,6 +71,14 @@ public class DimensionProperties implements Cloneable, IDimensionProperties { public static final int MIN_DISTANCE = 1; public static final int MAX_GRAVITY = 400; public static final int MIN_GRAVITY = 0; + /** + * A planet's rotational period when nothing else determines it: the default day length, and the + * scale the gravity-derived period is expressed in. Numerically equal to + * {@link zmaster587.advancedRocketry.util.AstronomicalBodyHelper#TICKS_PER_DAY} but a DIFFERENT + * quantity — that one is the platform's tick rate, this one is a per-planet property that most + * planets do not keep. Do not collapse them. + */ + public static final int DEFAULT_ROTATIONAL_PERIOD = 24000; public static final int WEATHER_START_LENGTH = 168000; public static final int WEATHER_PROLONGATION_LENGTH = 12000; @@ -449,7 +457,7 @@ public void resetProperties() { sunriseSunsetColors = new float[]{.7f, .2f, .2f, 1}; ringColor = new float[]{.4f, .4f, .7f}; gravitationalMultiplier = 1; - rotationalPeriod = 24000; + rotationalPeriod = DEFAULT_ROTATIONAL_PERIOD; orbitalDist = 100; originalAtmosphereDensity = atmosphereDensity = 100; childPlanets = new HashSet<>(); diff --git a/src/main/java/zmaster587/advancedRocketry/mixin/MixinWorldServer.java b/src/main/java/zmaster587/advancedRocketry/mixin/MixinWorldServer.java index e5d844620..57689ed78 100644 --- a/src/main/java/zmaster587/advancedRocketry/mixin/MixinWorldServer.java +++ b/src/main/java/zmaster587/advancedRocketry/mixin/MixinWorldServer.java @@ -69,7 +69,8 @@ public abstract class MixinWorldServer { */ private void ar$tellSleepersWhenDawnIs(WorldServer self) { int rotationalPeriod = self.provider instanceof IPlanetaryProvider - ? ((IPlanetaryProvider) self.provider).getRotationalPeriod(null) : 24000; + ? ((IPlanetaryProvider) self.provider).getRotationalPeriod(null) + : zmaster587.advancedRocketry.dimension.DimensionProperties.DEFAULT_ROTATIONAL_PERIOD; long ticksToDawn = ARDimensionWorldInfo.computeSleepWakeTime(self.getWorldTime(), rotationalPeriod) - self.getWorldTime(); // Real minutes, rounded up and never zero: "in 0 minutes" reads as a bug, and the player diff --git a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java index 3a8e81f44..a572af5a6 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java +++ b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java @@ -4,6 +4,43 @@ import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; public class AstronomicalBodyHelper { + + // ─── The reference frame ─────────────────────────────────────────────────── + // Named per MEANING, not per value. Three of these are 100 and they are NOT the same quantity: + // a distance scale, an atmosphere scale and a star-temperature scale all shared the literal in + // this one file, which made a search-and-replace on "100" a silent way to corrupt the + // temperature formula. Never collapse them because the numbers happen to match. + // + // They are ints so that every use site states the arithmetic it wants: the original mixed 100f + // and 100d for the SAME scale, and float-vs-double division is not always the same number once + // narrowed. The casts below are deliberate and preserve each site's original type exactly. + + /** Distance units in one astronomical unit — the scale the whole system is written in. */ + public static final int DISTANCE_UNITS_PER_AU = 100; + /** Atmosphere-density units in one Earth atmosphere. NOT the distance scale. */ + public static final int ATM_PRESSURE_UNITS_PER_ATMOSPHERE = 100; + /** Star-temperature units in one Sol. NOT the distance scale either. */ + public static final int TEMPERATURE_UNITS_PER_SOL = 100; + /** Kelvin per unit of {@link StellarBody#getTemperature()}. */ + public static final int KELVIN_PER_STAR_TEMPERATURE_UNIT = 58; + /** Solar radii in one astronomical unit — carries a star's size into the distance frame. */ + public static final int SOLAR_RADII_PER_AU = 215; + + // ─── The calendar ────────────────────────────────────────────────────────── + // Inherited from upstream: "One MC Year is 48 MC days (16 IRL Hours), one month is 8 MC Days". + // The two are leading coefficients of the same power law at two reference distances — one for + // planets around a star, one for moons around a planet. Their ratio (six months to a year) is a + // FICTION CHOICE, not a derivation, which is why the month is written independently rather than + // as a fraction of the year: changing one does NOT change the other. If that relation is ever + // meant to hold, encode it deliberately and record the decision here. + + /** Days in one year: the orbital period one AU from a size-1 star. */ + public static final int DAYS_PER_YEAR = 48; + /** Days in one lunar month: a moon's period at the reference distance from a mass-1 parent. */ + public static final int DAYS_PER_LUNAR_MONTH = 8; + /** Ticks in one day — the platform's rate, NOT a planet's rotational period (that is per-dim). */ + public static final int TICKS_PER_DAY = 24000; + /** * Returns the size multiplier for a body at the input distance, relative to either 1AU or the moon's orbital distance, depending on parent body * @@ -12,7 +49,7 @@ public class AstronomicalBodyHelper { */ public static float getBodySizeMultiplier(float orbitalDistance) { //Returns size multiplier relative to Earth standard (1AU = 100 Distance) - return 100f / orbitalDistance; + return (float) DISTANCE_UNITS_PER_AU / orbitalDistance; } /** @@ -24,7 +61,8 @@ public static float getBodySizeMultiplier(float orbitalDistance) { */ public static double getOrbitalPeriod(int orbitalDistance, float solarSize) { //One MC Year is 48 MC days (16 IRL Hours), one month is 8 MC Days - return 48d * Math.pow(Math.pow((orbitalDistance / (100d * solarSize)), 3), 0.5d); + return DAYS_PER_YEAR + * Math.pow(Math.pow((orbitalDistance / ((double) DISTANCE_UNITS_PER_AU * solarSize)), 3), 0.5d); } /** @@ -37,7 +75,8 @@ public static double getOrbitalPeriod(int orbitalDistance, float solarSize) { public static double getMoonOrbitalPeriod(float orbitalDistance, float planetaryMass) { //One (lunar) MC month is 8 MC days, so the moon orbits in 8 //The same a the function for planets, but since gravity is directly correlated with mass uses the gravity of the plant for mass - return 8d * Math.pow(Math.pow((orbitalDistance / 100d), 3) / planetaryMass, 0.5d); + return DAYS_PER_LUNAR_MONTH + * Math.pow(Math.pow((orbitalDistance / (double) DISTANCE_UNITS_PER_AU), 3) / planetaryMass, 0.5d); } /** @@ -60,7 +99,7 @@ public static double getOrbitalTheta(int orbitalDistance, float solarSize) { * @return the angle around the star in RADIANS */ public static double getOrbitalThetaAt(int orbitalDistance, float solarSize, long worldTick) { - double periodTicks = 24000d * getOrbitalPeriod(orbitalDistance, solarSize); + double periodTicks = (double) TICKS_PER_DAY * getOrbitalPeriod(orbitalDistance, solarSize); if (!(periodTicks > 0d) || Double.isInfinite(periodTicks)) { // A degenerate orbit (zero distance, or a star with no size recorded) does not move. // Answering 0 keeps it addressable instead of handing every caller a NaN coordinate. @@ -90,7 +129,8 @@ public static double getMoonOrbitalTheta(int orbitalDistance, float parentGravit public static double getMoonOrbitalThetaAt(int orbitalDistance, float parentGravitationalMultiplier, long worldTick) { //Because the function is still in AU and solar mass, some correctional factors to convert to those units - double periodTicks = 24000d * getMoonOrbitalPeriod(orbitalDistance, parentGravitationalMultiplier); + double periodTicks = (double) TICKS_PER_DAY + * getMoonOrbitalPeriod(orbitalDistance, parentGravitationalMultiplier); if (!(periodTicks > 0d) || Double.isInfinite(periodTicks)) { return 0d; } @@ -112,7 +152,7 @@ public static float getParentPlanetThetaFromMoon(int rotationalPeriod, int orbit float degreeOrbitalTheta = (float) (currentOrbitalTheta * 180 / Math.PI); //Computer the number of rotations per revolution and use that for how fast the planet would seem to orbit from the moon //Planet will not move at all if it is tidally locked - float planetPositionTheta = (((float) (AstronomicalBodyHelper.getMoonOrbitalPeriod(orbitalDistance, parentGravitationalMultiplier) * 24000) / rotationalPeriod) - 1) * degreeOrbitalTheta; + float planetPositionTheta = (((float) (AstronomicalBodyHelper.getMoonOrbitalPeriod(orbitalDistance, parentGravitationalMultiplier) * TICKS_PER_DAY) / rotationalPeriod) - 1) * degreeOrbitalTheta; //Add the base orbital theta so the planet is in the correct place return (planetPositionTheta + (float) (baseOrbitalTheta * 180 / Math.PI)) % 360; } @@ -126,15 +166,16 @@ public static float getParentPlanetThetaFromMoon(int rotationalPeriod, int orbit * @return the temperature of the planet in Kelvin */ public static int getAverageTemperature(StellarBody star, int orbitalDistance, int atmPressure) { - int starSurfaceTemperature = 58 * star.getTemperature(); - float starRadius = star.getSize() / 215f; + int starSurfaceTemperature = KELVIN_PER_STAR_TEMPERATURE_UNIT * star.getTemperature(); + float starRadius = star.getSize() / (float) SOLAR_RADII_PER_AU; //Gives output in AU - float planetaryOrbitalRadius = orbitalDistance / 100f; + float planetaryOrbitalRadius = orbitalDistance / (float) DISTANCE_UNITS_PER_AU; //Albedo is 0.3f hardcoded because of inability to easily calculate double averageWithoutAtmosphere = starSurfaceTemperature * Math.pow(starRadius / (2 * planetaryOrbitalRadius), 0.5) * Math.pow((1f - 0.3f), 0.25); //Slightly kludgey solution that works out mostly for Venus and well for Earth, without being overly complex //Output is in Kelvin - return (int) (averageWithoutAtmosphere * Math.max(1, (1.125d * Math.pow((atmPressure / 100d), 0.25)))); + return (int) (averageWithoutAtmosphere + * Math.max(1, (1.125d * Math.pow((atmPressure / (double) ATM_PRESSURE_UNITS_PER_ATMOSPHERE), 0.25)))); } /** @@ -153,8 +194,8 @@ public static double getStellarBrightness(StellarBody star, int orbitalDistance) //Normal stars are 1.0 times this value, black holes with accretion discs emit less and so modify it float lightMultiplier = 1.0f; //Make all values ratios of Earth normal to get ratio compared to Earth - float normalizedStarTemperature = star.getTemperature() / 100f; - float planetaryOrbitalRadius = orbitalDistance / 100f; + float normalizedStarTemperature = star.getTemperature() / (float) TEMPERATURE_UNITS_PER_SOL; + float planetaryOrbitalRadius = orbitalDistance / (float) DISTANCE_UNITS_PER_AU; //Check to see if the star is a black hole boolean blackHole = star.isBlackHole(); Iterable subs = star.getSubStars(); diff --git a/src/main/java/zmaster587/advancedRocketry/world/weather/ARDimensionWorldInfo.java b/src/main/java/zmaster587/advancedRocketry/world/weather/ARDimensionWorldInfo.java index 7b0e07aa2..06f3350f9 100644 --- a/src/main/java/zmaster587/advancedRocketry/world/weather/ARDimensionWorldInfo.java +++ b/src/main/java/zmaster587/advancedRocketry/world/weather/ARDimensionWorldInfo.java @@ -78,7 +78,7 @@ public ARDimensionWorldInfo(WorldInfo delegate, PlanetWeatherState weatherState, */ public static long computeSleepWakeTime(long current, int rotationalPeriod) { if (rotationalPeriod <= 0) { - rotationalPeriod = 24000; + rotationalPeriod = zmaster587.advancedRocketry.dimension.DimensionProperties.DEFAULT_ROTATIONAL_PERIOD; } long next = current + rotationalPeriod; return next - Math.floorMod(next, (long) rotationalPeriod); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java index 86f6c9c27..f0207781e 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java @@ -147,4 +147,75 @@ public void planetaryLightMultiplierWithinExpectedBounds() { } } + // ───────────────────────────────────────────────────────────────────────────── + // The reference frame, pinned by VALUE. + // + // The assertions above are mostly relative — thicker is warmer, farther is cooler — and a + // relative assertion cannot notice that a scale constant moved: rescale the atmosphere axis and + // "thicker is warmer" still holds while every temperature is wrong. These pin the absolute + // numbers instead, each derived from the frame's own definitions (100 distance units = 1 AU, + // 48 days = a year, 8 = a lunar month) rather than recorded from a run. + // + // They exist so that naming the scale constants can be shown to change nothing — and they stay + // afterwards as the guard for the next edit. The temperature ones matter most: the distance + // scale and the atmosphere scale are both 100 and live four lines apart, so a well-meant + // search-and-replace can silently corrupt one of them. + // ───────────────────────────────────────────────────────────────────────────── + + @Test + public void orbitalPeriodFollowsTheThreeHalvesPowerLawExactly() { + // Four times the distance is eight times the period. + assertEquals(384.0, AstronomicalBodyHelper.getOrbitalPeriod(400, 1.0f), 1e-9); + // A bigger star pulls the same distance into a shorter year. + assertEquals(31.176914536239792, AstronomicalBodyHelper.getOrbitalPeriod(150, 2.0f), 1e-9); + } + + @Test + public void moonPeriodScalesWithParentMassAndDistanceExactly() { + // Four times the parent mass halves the period. + assertEquals(4.0, AstronomicalBodyHelper.getMoonOrbitalPeriod(100f, 4.0f), 1e-9); + assertEquals(22.627416997969522, AstronomicalBodyHelper.getMoonOrbitalPeriod(200f, 1.0f), 1e-9); + } + + @Test + public void temperatureAtOneAuUnderOneAtmosphereIsPinned() { + // 1 AU, one atmosphere: the radiative balance times the greenhouse term. + assertEquals(287, AstronomicalBodyHelper.getAverageTemperature(sunLikeStar(), 100, 100)); + } + + @Test + public void aVacuumWorldGetsTheBareRadiativeBalance() { + // atmPressure 0 falls to the max(1, ...) floor — no greenhouse lift at all. + assertEquals(255, AstronomicalBodyHelper.getAverageTemperature(sunLikeStar(), 100, 0)); + } + + @Test + public void temperatureAtFourAuIsPinned() { + assertEquals(143, AstronomicalBodyHelper.getAverageTemperature(sunLikeStar(), 400, 100)); + } + + @Test + public void brightnessFallsWithTheSquareOfDistanceExactly() { + assertEquals(0.25, AstronomicalBodyHelper.getStellarBrightness(sunLikeStar(), 200), 1e-9); + } + + // The tick-taking overloads of the theta helpers do NOT touch the mod proxy — only the no-arg + // forms do, which is what the class note above excludes. They carry the same law, so the wrap + // is checkable here as well as in the integration test. + + @Test + public void orbitalThetaWrapsOncePerPeriod() { + long periodTicks = (long) (48.0 * 24000.0); + assertEquals(0.0, AstronomicalBodyHelper.getOrbitalThetaAt(100, 1.0f, 0L), 1e-9); + assertEquals(Math.PI / 2.0, + AstronomicalBodyHelper.getOrbitalThetaAt(100, 1.0f, periodTicks / 4L), 1e-9); + assertEquals(0.0, AstronomicalBodyHelper.getOrbitalThetaAt(100, 1.0f, periodTicks), 1e-9); + } + + @Test + public void aDegenerateOrbitStaysAddressableRatherThanNaN() { + assertEquals(0.0, AstronomicalBodyHelper.getOrbitalThetaAt(0, 1.0f, 12345L), 1e-9); + assertEquals(0.0, AstronomicalBodyHelper.getMoonOrbitalThetaAt(100, 0f, 12345L), 1e-9); + } + } From a91ffbc76452a6e482fd4246cafc013568e54568 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Tue, 11 Aug 2026 09:41:18 +0300 Subject: [PATCH 02/42] fix: a planet publishes its own world type, not the save's - install ARPlanetWorldInfo before the provider caches its values - terrainGeneratorOptions per dimension, in XML and NBT - one TerrainResolution so provider and WorldInfo cannot disagree - forward getGeneratorOptions through the weather wrapper - drop two setTerrainType calls that never did anything - report the published identity from dim info and report_state --- .../command/test/TestProbeCommand.java | 17 ++- .../dimension/DimensionProperties.java | 20 ++++ .../dimension/TerrainResolution.java | 69 ++++++++++++ .../mixin/MixinWorldProvider.java | 34 ++++++ .../util/XMLPlanetLoader.java | 6 ++ .../world/ARPlanetWorldInfo.java | 102 ++++++++++++++++++ .../world/provider/WorldProviderAsteroid.java | 2 - .../world/provider/WorldProviderPlanet.java | 47 +++----- .../world/weather/ARDimensionWorldInfo.java | 11 ++ .../resources/mixins.advancedrocketry.json | 1 + .../client/AbstractSharedClientE2ETest.java | 8 ++ .../WorldCommandClientGroupE2ETest.java | 53 +++++++++ .../server/PlanetTerrainSourceE2ETest.java | 94 ++++++++++++++++ .../forge/testing/client/ClientBot.java | 5 + .../bridge/ForgeTestClientBootstrap.java | 10 ++ 15 files changed, 444 insertions(+), 35 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/dimension/TerrainResolution.java create mode 100644 src/main/java/zmaster587/advancedRocketry/mixin/MixinWorldProvider.java create mode 100644 src/main/java/zmaster587/advancedRocketry/world/ARPlanetWorldInfo.java diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 85942bfc6..04517d03e 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -4288,6 +4288,19 @@ private void handleDim(ICommandSender sender, String[] args) { info.put("chunkGeneratorClass", chunkGeneratorClassOf(world)); info.put("saveDir", (world != null && world.provider.getSaveFolder() != null) ? world.provider.getSaveFolder() : "null"); + // The world-generation identity this dimension PUBLISHES through the vanilla WorldInfo + // API — the channel a foreign WorldType reads when it configures itself. It is reported + // next to the overworld's own value because the failure mode is not "wrong name" but + // "somebody else's name": a secondary world's WorldInfo delegates both of these, so a + // planet can silently answer with the save's world type and an empty options string. + info.put("worldType", (world != null && world.getWorldInfo().getTerrainType() != null) + ? world.getWorldInfo().getTerrainType().getName() : "null"); + info.put("generatorOptions", world != null ? world.getWorldInfo().getGeneratorOptions() : "null"); + net.minecraft.world.WorldServer overworld = net.minecraftforge.common.DimensionManager.getWorld(0); + info.put("overworldWorldType", (overworld != null && overworld.getWorldInfo().getTerrainType() != null) + ? overworld.getWorldInfo().getTerrainType().getName() : "null"); + info.put("overworldGeneratorOptions", + overworld != null ? overworld.getWorldInfo().getGeneratorOptions() : "null"); info.put("isARPlanet", DimensionManager.getInstance().isDimensionCreated(dim)); if (props != null) { info.put("name", props.getName()); @@ -9239,7 +9252,7 @@ private void handleWorldgen(MinecraftServer server, ICommandSender sender, Strin return; } if (args.length >= 4 && "create-terrain-dim".equalsIgnoreCase(args[0])) { - // worldgen create-terrain-dim [param] + // worldgen create-terrain-dim [param] [generatorOptions] // Register a new PLANET dimension by cloning an existing AR planet's // DimensionProperties (inheriting star / atmosphere / gravity linkage so // headless worldprovider-init doesn't NPE), re-id'ing it, and setting a @@ -9251,6 +9264,7 @@ private void handleWorldgen(MinecraftServer server, ICommandSender sender, Strin zmaster587.advancedRocketry.dimension.TerrainSource terrain = zmaster587.advancedRocketry.dimension.TerrainSource.byName(args[3]); String param = args.length >= 5 ? args[4] : ""; + String generatorOptions = args.length >= 6 ? args[5] : ""; zmaster587.advancedRocketry.dimension.DimensionManager dm = zmaster587.advancedRocketry.dimension.DimensionManager.getInstance(); if (dm.isDimensionCreated(newId)) { @@ -9275,6 +9289,7 @@ private void handleWorldgen(MinecraftServer server, ICommandSender sender, Strin props.setTerrainWorldType(param); else if (terrain == zmaster587.advancedRocketry.dimension.TerrainSource.TEMPLATE) props.setTerrainTemplate(param); + props.setTerrainGeneratorOptions(generatorOptions); boolean registered = dm.registerDim(props, true); // Belt-and-braces: ensure Forge knows the dim under the planet provider // even if registerDim's internal guard skipped it. diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java index 32403b53f..4e73ab4d6 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java @@ -189,6 +189,13 @@ private static float clampFeatureFrequencyMultiplier(float multiplier) { private TerrainSource terrainSource = TerrainSource.NATIVE; private String terrainWorldType = ""; // foreign WorldType name for MOD_WORLDTYPE private String terrainTemplate = ""; // template folder name for TEMPLATE + /** + * The settings string handed to this dimension's chunk generator — vanilla's "generator options", + * per dimension instead of per save. A foreign {@link net.minecraft.world.WorldType} receives it + * as the second argument of {@code getChunkGenerator}, and reads it back off this world's + * {@code WorldInfo} when it identifies itself; an empty string means "your defaults". + */ + private String terrainGeneratorOptions = ""; //public int target_sea_level; // modId must be declared explicitly: this @SidedProxy lives outside the @Mod class, and the jar @@ -255,6 +262,7 @@ public DimensionProperties(int id) { terrainSource = TerrainSource.NATIVE; terrainWorldType = ""; terrainTemplate = ""; + terrainGeneratorOptions = ""; //target_sea_level = seaLevel; //water_can_exist = true; @@ -477,6 +485,7 @@ public void resetProperties() { terrainSource = TerrainSource.NATIVE; terrainWorldType = ""; terrainTemplate = ""; + terrainGeneratorOptions = ""; laserDrillOres = new ArrayList<>(); } @@ -1713,6 +1722,7 @@ else if (nbt.hasKey("biomes", NBT.TAG_INT_ARRAY)) { terrainSource = nbt.hasKey("terrainSource") ? TerrainSource.byName(nbt.getString("terrainSource")) : TerrainSource.NATIVE; terrainWorldType = nbt.getString("terrainWorldType"); terrainTemplate = nbt.getString("terrainTemplate"); + terrainGeneratorOptions = nbt.getString("terrainGeneratorOptions"); canGenerateCraters = nbt.getBoolean("canGenerateCraters"); canGenerateGeodes = nbt.getBoolean("canGenerateGeodes"); canGenerateStructures = nbt.getBoolean("canGenerateStructures"); @@ -2088,6 +2098,8 @@ public void writeToNBT(NBTTagCompound nbt) { nbt.setString("terrainWorldType", terrainWorldType); if (!terrainTemplate.isEmpty()) nbt.setString("terrainTemplate", terrainTemplate); + if (!terrainGeneratorOptions.isEmpty()) + nbt.setString("terrainGeneratorOptions", terrainGeneratorOptions); nbt.setBoolean("canGenerateCraters", canGenerateCraters); nbt.setBoolean("canGenerateGeodes", canGenerateGeodes); nbt.setBoolean("canGenerateStructures", canGenerateStructures); @@ -2369,6 +2381,14 @@ public void setTerrainTemplate(String terrainTemplate) { this.terrainTemplate = terrainTemplate == null ? "" : terrainTemplate; } + public String getTerrainGeneratorOptions() { + return terrainGeneratorOptions; + } + + public void setTerrainGeneratorOptions(String terrainGeneratorOptions) { + this.terrainGeneratorOptions = terrainGeneratorOptions == null ? "" : terrainGeneratorOptions; + } + public void setGenerateCraters(boolean canGenerateCraters) { this.canGenerateCraters = canGenerateCraters; } diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/TerrainResolution.java b/src/main/java/zmaster587/advancedRocketry/dimension/TerrainResolution.java new file mode 100644 index 000000000..9a01a68ce --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/dimension/TerrainResolution.java @@ -0,0 +1,69 @@ +package zmaster587.advancedRocketry.dimension; + +import net.minecraft.world.WorldType; +import zmaster587.advancedRocketry.AdvancedRocketry; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Resolves what a planet dimension's terrain is ACTUALLY produced by, after the fallbacks: an + * authored {@link TerrainSource} plus, for {@link TerrainSource#MOD_WORLDTYPE}, the foreign + * {@link WorldType} it names. + * + *

This exists as one shared answer rather than one per caller because two very different places + * need it and must not be able to disagree: the {@code WorldProviderPlanet} that picks the chunk + * generator, and the per-dimension {@code WorldInfo} that publishes this world's generation identity + * to third-party code. A planet that generates with a foreign world type while telling everyone it + * is something else is the defect this class prevents from being re-introduced.

+ * + *

Fallbacks are deliberate and quiet-ish: a MOD_WORLDTYPE naming a world type no installed mod + * registered, or a TEMPLATE with no template path, degrades to {@link TerrainSource#NATIVE} with one + * warning per dimension, so a mis-authored planet still generates instead of failing to load.

+ */ +public final class TerrainResolution { + + /** Dimensions already warned about, so a per-chunk or per-lookup resolve cannot spam the log. */ + private static final Set warnedDims = Collections.synchronizedSet(new HashSet()); + + /** The terrain source actually in force — never the authored value if that value was unusable. */ + public final TerrainSource source; + /** + * The world type this dimension actually generates with: the foreign one when {@link #source} is + * {@link TerrainSource#MOD_WORLDTYPE}, otherwise Advanced Rocketry's own planet world type. + * Null only if AR's world type has not been registered yet (before {@code FMLInitializationEvent}). + */ + public final WorldType worldType; + + private TerrainResolution(TerrainSource source, WorldType worldType) { + this.source = source; + this.worldType = worldType; + } + + /** @param props this dimension's properties; must not be null (a non-AR dimension has no resolution). */ + public static TerrainResolution of(int dim, DimensionProperties props) { + TerrainSource requested = props.getTerrainSource(); + + if (requested == TerrainSource.MOD_WORLDTYPE) { + String name = props.getTerrainWorldType(); + WorldType foreign = (name == null || name.isEmpty()) ? null : WorldType.parseWorldType(name); + if (foreign != null) + return new TerrainResolution(TerrainSource.MOD_WORLDTYPE, foreign); + warnOnce(dim, "requests MOD_WORLDTYPE '" + name + + "' which is not registered; falling back to NATIVE terrain"); + } else if (requested == TerrainSource.TEMPLATE) { + String template = props.getTerrainTemplate(); + if (template != null && !template.isEmpty()) + return new TerrainResolution(TerrainSource.TEMPLATE, AdvancedRocketry.planetWorldType); + warnOnce(dim, "requests TEMPLATE terrain with no template path; falling back to NATIVE"); + } + + return new TerrainResolution(TerrainSource.NATIVE, AdvancedRocketry.planetWorldType); + } + + private static void warnOnce(int dim, String message) { + if (warnedDims.add(dim)) + AdvancedRocketry.logger.warn("Planet dimension " + dim + " " + message); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/mixin/MixinWorldProvider.java b/src/main/java/zmaster587/advancedRocketry/mixin/MixinWorldProvider.java new file mode 100644 index 000000000..58d6a683f --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/mixin/MixinWorldProvider.java @@ -0,0 +1,34 @@ +package zmaster587.advancedRocketry.mixin; + +import net.minecraft.world.World; +import net.minecraft.world.WorldProvider; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import zmaster587.advancedRocketry.world.ARPlanetWorldInfo; + +/** + * Installs Advanced Rocketry's per-dimension {@link net.minecraft.world.storage.WorldInfo} on an AR + * dimension, at the only moment early enough to matter. + * + *

{@code WorldProvider.setWorld} is where vanilla caches this world's terrain type and generator + * options into private fields ({@code WorldProvider:52-53}) and then calls {@code init()}; the + * enclosing {@code WorldServer} constructor builds the chunk provider on the very next line. Anything + * that swaps the {@code WorldInfo} later — a {@code WorldEvent.Load} handler, say — arrives after the + * biome provider and the chunk generator have already been built from the OVERWORLD's values. + * Injecting at HEAD puts the right info in place before any of that reads it.

+ * + *

Deliberately NOT gated by the {@code perDimWorldInfo} config flag: that flag governs per-planet + * weather and time, and which terrain a planet generates is not weather's business. The guard lives + * in {@link ARPlanetWorldInfo#installIfNeeded(World)} instead, which touches only server-side AR + * dimensions whose info is still vanilla's shared-overworld one.

+ */ +@Mixin(WorldProvider.class) +public abstract class MixinWorldProvider { + + @Inject(method = "setWorld", at = @At("HEAD")) + private void ar$installPerDimensionWorldInfo(World worldIn, CallbackInfo ci) { + ARPlanetWorldInfo.installIfNeeded(worldIn); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index c2815343b..588da3818 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -107,6 +107,7 @@ public class XMLPlanetLoader { private static final String ELEMENT_TERRAIN_SOURCE = "terrainSource"; private static final String ELEMENT_TERRAIN_WORLDTYPE = "terrainWorldType"; private static final String ELEMENT_TERRAIN_TEMPLATE = "terrainTemplate"; + private static final String ELEMENT_TERRAIN_GENERATOR_OPTIONS = "terrainGeneratorOptions"; private static final String ELEMENT_RIVER_OVERRIDE = "forceRiverGeneration"; private static final String ELEMENT_OREGEN = "oreGen"; private static final String ELEMENT_LASER_DRILL_ORES = "laserDrillOres"; @@ -448,6 +449,8 @@ private static Node writePlanet(Document doc, DimensionProperties properties) { nodePlanet.appendChild(createTextNode(doc, ELEMENT_TERRAIN_WORLDTYPE, properties.getTerrainWorldType())); if (!properties.getTerrainTemplate().isEmpty()) nodePlanet.appendChild(createTextNode(doc, ELEMENT_TERRAIN_TEMPLATE, properties.getTerrainTemplate())); + if (!properties.getTerrainGeneratorOptions().isEmpty()) + nodePlanet.appendChild(createTextNode(doc, ELEMENT_TERRAIN_GENERATOR_OPTIONS, properties.getTerrainGeneratorOptions())); if (properties.oreProperties != null) { nodePlanet.appendChild(XMLOreLoader.writeOreEntryXML(doc, properties.oreProperties)); @@ -1074,6 +1077,9 @@ else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_BIOMEIDS)) { properties.setTerrainWorldType(planetPropertyNode.getTextContent().trim()); } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_TERRAIN_TEMPLATE)) { properties.setTerrainTemplate(planetPropertyNode.getTextContent().trim()); + } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_TERRAIN_GENERATOR_OPTIONS)) { + // NOT trimmed: a generator settings string is opaque to us and may be whitespace-significant. + properties.setTerrainGeneratorOptions(planetPropertyNode.getTextContent()); } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_HASRINGS)) properties.hasRings = Boolean.parseBoolean(planetPropertyNode.getTextContent()); else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_CAN_DECORATE)) diff --git a/src/main/java/zmaster587/advancedRocketry/world/ARPlanetWorldInfo.java b/src/main/java/zmaster587/advancedRocketry/world/ARPlanetWorldInfo.java new file mode 100644 index 000000000..4c47f089d --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/world/ARPlanetWorldInfo.java @@ -0,0 +1,102 @@ +package zmaster587.advancedRocketry.world; + +import net.minecraft.world.WorldType; +import net.minecraft.world.storage.DerivedWorldInfo; +import net.minecraft.world.storage.WorldInfo; +import zmaster587.advancedRocketry.dimension.DimensionManager; +import zmaster587.advancedRocketry.dimension.DimensionProperties; +import zmaster587.advancedRocketry.dimension.TerrainResolution; + +/** + * The {@link WorldInfo} an Advanced Rocketry dimension publishes, replacing the plain + * {@link DerivedWorldInfo} vanilla installs on every secondary world. + * + *

It answers exactly two questions per dimension instead of per save — the world type and the + * generator options string — and inherits the delegation of everything else, so nothing about the + * shared level state changes. Two questions, because those two are what a third-party + * {@link WorldType} reads when it identifies and configures itself, and Advanced Rocketry cannot + * patch a foreign generator's read sites. Vanilla's {@code DerivedWorldInfo} answers both about the + * OVERWORLD: {@code setTerrainType} is an empty method there, so a planet's attempt to stamp its own + * is silently dropped, and {@code getGeneratorOptions} is never overridden at all, so the string is + * always empty.

+ * + *

Why it is installed in the constructor and not from a world event: {@code WorldServer}'s + * constructor calls {@code provider.setWorld(this)} and then {@code createChunkProvider()} before it + * returns, and {@code WorldProvider.setWorld} caches both of these values into private fields. A + * {@code WorldInfo} swapped in later — at {@code WorldEvent.Load}, say — is already too late to + * reach the biome provider or the chunk generator, which is the whole point of having it.

+ * + *

Values are read live from {@link DimensionProperties} rather than snapshotted: the properties + * are the source of truth, and a copy taken at construction would go stale the moment a dimension's + * terrain is re-authored.

+ */ +public class ARPlanetWorldInfo extends DerivedWorldInfo { + + private final int dimension; + private final WorldInfo delegate; + + public ARPlanetWorldInfo(WorldInfo delegate, int dimension) { + super(delegate); + this.delegate = delegate; + this.dimension = dimension; + } + + /** The dimension this info speaks for. */ + public int getDimension() { + return dimension; + } + + /** + * Replaces {@code world}'s vanilla {@link DerivedWorldInfo} with a per-dimension one, if this is + * an Advanced Rocketry dimension that still carries the shared-overworld info. Idempotent, and a + * no-op for every world it is not about: the client, the overworld, and any dimension AR did not + * create keep exactly the info they had. + * + * @return whether an info was installed by this call + */ + public static boolean installIfNeeded(net.minecraft.world.World world) { + if (!(world instanceof net.minecraft.world.WorldServer)) + return false; + if (world.provider == null) + return false; + WorldInfo current = world.getWorldInfo(); + // Only vanilla's shared-overworld info is replaced. Anything else is either the overworld's + // real WorldInfo, our own (already installed), or another mod's — none of them ours to swap. + if (!(current instanceof DerivedWorldInfo) || current instanceof ARPlanetWorldInfo) + return false; + int dim = world.provider.getDimension(); + if (dim == 0 || !DimensionManager.getInstance().isDimensionCreated(dim)) + return false; + world.worldInfo = new ARPlanetWorldInfo(current, dim); + return true; + } + + @Override + public WorldType getTerrainType() { + TerrainResolution resolved = resolve(); + if (resolved == null || resolved.worldType == null) + return delegate.getTerrainType(); + return resolved.worldType; + } + + /** + * Kept a no-op like the superclass. The per-dimension world type is derived from + * {@link DimensionProperties}, so accepting a write here would create a second source of truth + * that only the writer could see — and every existing caller of this setter is passing the value + * this class already derives. + */ + @Override + public void setTerrainType(WorldType type) { + } + + @Override + public String getGeneratorOptions() { + DimensionProperties props = DimensionManager.getInstance().getDimensionProperties(dimension); + return props == null ? delegate.getGeneratorOptions() : props.getTerrainGeneratorOptions(); + } + + private TerrainResolution resolve() { + DimensionProperties props = DimensionManager.getInstance().getDimensionProperties(dimension); + return props == null ? null : TerrainResolution.of(dimension, props); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderAsteroid.java b/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderAsteroid.java index f4638dc29..705a016a5 100644 --- a/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderAsteroid.java +++ b/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderAsteroid.java @@ -6,7 +6,6 @@ import net.minecraftforge.client.IRenderHandler; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; -import zmaster587.advancedRocketry.AdvancedRocketry; import zmaster587.advancedRocketry.api.ARConfiguration; import zmaster587.advancedRocketry.api.AdvancedRocketryBiomes; import zmaster587.advancedRocketry.client.render.planet.RenderAsteroidSky; @@ -52,7 +51,6 @@ public float calculateCelestialAngle(long worldTime, float p_76563_3_) { @Override protected void init() { this.hasSkyLight = true; - world.getWorldInfo().setTerrainType(AdvancedRocketry.planetWorldType); this.biomeProvider = new BiomeProviderSingle(AdvancedRocketryBiomes.spaceBiome);//new ChunkManagerPlanet(worldObj, worldObj.getWorldInfo().getGeneratorOptions(), DimensionManager.getInstance().getDimensionProperties(worldObj.provider.getDimension()).getBiomes()); diff --git a/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderPlanet.java b/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderPlanet.java index 40ba0fa86..290af794d 100644 --- a/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderPlanet.java +++ b/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderPlanet.java @@ -18,7 +18,6 @@ import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import org.apache.commons.lang3.ArrayUtils; -import zmaster587.advancedRocketry.AdvancedRocketry; import zmaster587.advancedRocketry.api.ARConfiguration; import zmaster587.advancedRocketry.api.AdvancedRocketryItems; import zmaster587.advancedRocketry.api.IAtmosphere; @@ -30,6 +29,7 @@ import zmaster587.advancedRocketry.client.render.planet.RenderPlanetarySky; import zmaster587.advancedRocketry.dimension.DimensionManager; import zmaster587.advancedRocketry.dimension.DimensionProperties; +import zmaster587.advancedRocketry.dimension.TerrainResolution; import zmaster587.advancedRocketry.dimension.TerrainSource; import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; import zmaster587.advancedRocketry.world.ChunkManagerPlanet; @@ -69,48 +69,32 @@ public IChunkGenerator createChunkGenerator() { resolveTerrainSource(); if (effectiveTerrainSource == TerrainSource.MOD_WORLDTYPE) - return foreignWorldType.getChunkGenerator(world, world.getWorldInfo().getGeneratorOptions()); + return foreignWorldType.getChunkGenerator(world, generatorOptions()); if (effectiveTerrainSource == TerrainSource.TEMPLATE) return new ChunkProviderTemplate(this.world); int genType = DimensionManager.getInstance().getDimensionProperties(world.provider.getDimension()).getGenType(); if (genType == 1) { - return new ChunkProviderCavePlanet(this.world, false, this.world.getSeed(), world.getWorldInfo().getGeneratorOptions()); + return new ChunkProviderCavePlanet(this.world, false, this.world.getSeed(), generatorOptions()); } else - return new ChunkProviderPlanet(this.world, this.world.getSeed(), ARConfiguration.getCurrentConfig().generateVanillaStructures, world.getWorldInfo().getGeneratorOptions()); + return new ChunkProviderPlanet(this.world, this.world.getSeed(), ARConfiguration.getCurrentConfig().generateVanillaStructures, generatorOptions()); } /** * Resolves {@link #effectiveTerrainSource} (and {@link #foreignWorldType}) once from this dimension's - * {@link DimensionProperties}. A MOD_WORLDTYPE whose name is blank or unregistered, or a TEMPLATE with no - * template path, falls back to NATIVE with a warning so a mis-authored planet still generates. + * {@link DimensionProperties}, through the shared {@link TerrainResolution} so that this provider and + * the dimension's {@code WorldInfo} cannot answer differently about the same planet. */ private void resolveTerrainSource() { - DimensionProperties props = getDimensionProperties(); - TerrainSource requested = props.getTerrainSource(); - if (requested == TerrainSource.MOD_WORLDTYPE) { - String name = props.getTerrainWorldType(); - foreignWorldType = (name == null || name.isEmpty()) ? null : WorldType.parseWorldType(name); - if (foreignWorldType == null) { - AdvancedRocketry.logger.warn("Planet dimension " + getDimension() + " requests MOD_WORLDTYPE '" + name - + "' which is not registered; falling back to NATIVE terrain"); - effectiveTerrainSource = TerrainSource.NATIVE; - } else { - effectiveTerrainSource = TerrainSource.MOD_WORLDTYPE; - } - } else if (requested == TerrainSource.TEMPLATE) { - String template = props.getTerrainTemplate(); - if (template == null || template.isEmpty()) { - AdvancedRocketry.logger.warn("Planet dimension " + getDimension() - + " requests TEMPLATE terrain with no template path; falling back to NATIVE"); - effectiveTerrainSource = TerrainSource.NATIVE; - } else { - effectiveTerrainSource = TerrainSource.TEMPLATE; - } - } else { - effectiveTerrainSource = TerrainSource.NATIVE; - } + TerrainResolution resolved = TerrainResolution.of(getDimension(), getDimensionProperties()); + effectiveTerrainSource = resolved.source; + foreignWorldType = resolved.source == TerrainSource.MOD_WORLDTYPE ? resolved.worldType : null; + } + + /** The settings string this dimension's chunk generator is configured with. Per dimension, not per save. */ + private String generatorOptions() { + return getDimensionProperties().getTerrainGeneratorOptions(); } @Override @@ -144,7 +128,6 @@ public BiomeGenBase getBiomeGenForCoords(int x, int z) { @Override protected void init() { this.hasSkyLight = true; - world.getWorldInfo().setTerrainType(AdvancedRocketry.planetWorldType); resolveTerrainSource(); @@ -153,7 +136,7 @@ protected void init() { if (effectiveTerrainSource == TerrainSource.MOD_WORLDTYPE) this.biomeProvider = foreignWorldType.getBiomeProvider(world); else - this.biomeProvider = new ChunkManagerPlanet(world, world.getWorldInfo().getGeneratorOptions(), DimensionManager.getInstance().getDimensionProperties(world.provider.getDimension()).getBiomes()); + this.biomeProvider = new ChunkManagerPlanet(world, generatorOptions(), DimensionManager.getInstance().getDimensionProperties(world.provider.getDimension()).getBiomes()); //AdvancedRocketry.planetWorldType.getChunkManager(worldObj); } diff --git a/src/main/java/zmaster587/advancedRocketry/world/weather/ARDimensionWorldInfo.java b/src/main/java/zmaster587/advancedRocketry/world/weather/ARDimensionWorldInfo.java index 06f3350f9..98172a35a 100644 --- a/src/main/java/zmaster587/advancedRocketry/world/weather/ARDimensionWorldInfo.java +++ b/src/main/java/zmaster587/advancedRocketry/world/weather/ARDimensionWorldInfo.java @@ -304,6 +304,17 @@ public WorldType getTerrainType() { public void setTerrainType(WorldType type) { } + /** + * Delegated like every other read. This wrapper's own {@code super()} state is inert, so an + * un-overridden getter answers from a {@link WorldInfo} that was never populated — here that + * would be the empty string, silently replacing whatever the wrapped info publishes and + * un-configuring the dimension's chunk generator. + */ + @Override + public String getGeneratorOptions() { + return delegate.getGeneratorOptions(); + } + @Override public boolean areCommandsAllowed() { return delegate.areCommandsAllowed(); diff --git a/src/main/resources/mixins.advancedrocketry.json b/src/main/resources/mixins.advancedrocketry.json index 6714a5ebe..b71dfb5c1 100644 --- a/src/main/resources/mixins.advancedrocketry.json +++ b/src/main/resources/mixins.advancedrocketry.json @@ -16,6 +16,7 @@ "MixinEntityPlayerMPInventoryAccess", "MixinPlayerList", "MixinTileAdvancedFlightComputer", + "MixinWorldProvider", "MixinWorldServer", "MixinWorldServerMulti", "MixinWorldServerShipManager", diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/AbstractSharedClientE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/AbstractSharedClientE2ETest.java index 181301892..40c718354 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/AbstractSharedClientE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/AbstractSharedClientE2ETest.java @@ -297,6 +297,14 @@ private void resetBetweenScenarios() throws Exception { String screen = state.has("screen") ? state.get("screen").getAsString() : ""; int overlayTicks = chat.has("overlayTicks") ? chat.get("overlayTicks").getAsInt() : -1; int chatLines = chat.has("count") ? chat.get("count").getAsInt() : -1; + // `report_state` omits the player block entirely when the client has no player — mid + // dimension change, or after the connection died. Dereferencing it raises a bare NPE from + // this line, which names the RESET as the failure and hides the previous scenario that + // actually wedged the client; measured 2026-08-10, that cost an investigation aimed at an + // unrelated production change for want of one sentence. + assertTrue("the client has no player, so this scenario cannot be arranged at all — the" + + " PREVIOUS scenario left the connection or the dimension change unfinished." + + " client state=" + state, state.has("playerX") && state.has("playerZ")); double px = state.get("playerX").getAsDouble(); double pz = state.get("playerZ").getAsDouble(); diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandClientGroupE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandClientGroupE2ETest.java index 6a62ff299..47e90d30b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandClientGroupE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandClientGroupE2ETest.java @@ -65,6 +65,9 @@ public class WorldCommandClientGroupE2ETest extends AbstractSharedClientE2ETest /** The space dim, where {@code /ar goto station} lands the player. */ private static final int SPACE_DIM = -2; + /** The registered name of AR's planet world type (WorldTypePlanetGen). */ + private static final String AR_PLANET_WORLD_TYPE = "PlanetGen"; + @Override protected String subsystem() { return "world-command"; @@ -262,6 +265,56 @@ public void arGotoTransfersPlayerToTargetDim() throws Exception { } } + /** + * The planet's own world type has to reach the CLIENT, because client-side terrain code + * identifies a world by it — and a secondary world's {@code WorldInfo} used to answer with the + * SAVE's world type, so every planet a player entered claimed to be the overworld's kind. + * + *

The overworld's value is read first, in this same scenario, and the assertion is that the + * value CHANGED on crossing. Asserting the planet's name alone would also pass on a build that + * hard-codes one world type everywhere, which is the failure this is about.

+ */ + @Test + public void arGotoMakesTheClientRenderThePlanetsOwnWorldType() throws Exception { + scenario().arranging("op the bot and generate a planet to travel to"); + opTheBot(); + String home = clientWorldType(); + scenario().record("homeWorldType", home); + scenario().requireArranged("the client must name the world type it starts in, else the" + + " comparison below has nothing to change FROM; got '" + home + "'", !home.isEmpty()); + + String before = exec("ar planet list"); + exec("ar planet generate 0 WorldTypeTarget 10 10 10"); + String after = exec("ar planet list"); + int targetDim = newDimFromDiff(before, after); + scenario().record("targetDim", targetDim); + scenario().requireArranged("planet generate must yield a new dim id; before=" + before + + " after=" + after, targetDim != -1); + try { + exec("artest dim load " + targetDim); + + scenario().asserting("the client renders the planet's own world type after arriving"); + bot().sendChat("/ar goto dimension " + targetDim); + waitForClientDim(targetDim); + + String onPlanet = clientWorldType(); + scenario().record("planetWorldType", onPlanet); + assertEquals("the client must learn the planet's own world type on arrival, not the" + + " one the save was created with", AR_PLANET_WORLD_TYPE, onPlanet); + assertNotEquals("the world type the client renders must differ between the overworld" + + " and a planet, or it is not per-dimension at all", home, onPlanet); + } finally { + exec("artest tp " + plot().dim); + exec("ar planet delete " + targetDim); + } + } + + /** The world type the CLIENT believes it is in, by name. */ + private String clientWorldType() throws Exception { + JsonObject state = bot().reportState(); + return state != null && state.has("worldType") ? state.get("worldType").getAsString() : ""; + } + // ── /ar goto station ────────────────────────────────────────────────────── /** From {@code WorldCommandPlayerEquippedE2ETest}: a station's spawn is in the space dim, and diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/PlanetTerrainSourceE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/PlanetTerrainSourceE2ETest.java index 0878bd3a9..421adfade 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/PlanetTerrainSourceE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/PlanetTerrainSourceE2ETest.java @@ -6,6 +6,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -32,6 +33,13 @@ public class PlanetTerrainSourceE2ETest extends AbstractSharedServerTest { private static final int MOD_WT_DIM = 9990; private static final int TEMPLATE_DIM = 9991; private static final int FALLBACK_DIM = 9992; + private static final int OPTIONS_DIM = 9993; + + /** The registered name of {@code AdvancedRocketry.planetWorldType} (see {@code WorldTypePlanetGen}). */ + private static final String AR_PLANET_WORLD_TYPE = "PlanetGen"; + + /** A flat preset no default world could produce, so "the options arrived" is visible in blocks. */ + private static final String FLAT_DIAMOND_PRESET = "3;minecraft:bedrock,3*minecraft:diamond_block;1"; private static final String AR_PLANET_PROVIDER = "\"providerClass\":\"zmaster587.advancedRocketry.world.provider.WorldProviderPlanet\""; @@ -108,6 +116,92 @@ public void unregisteredModWorldtypeFallsBackToNativeGenerator() throws Exceptio info.contains("ChunkGeneratorFlat")); } + /** + * A planet publishes ITS OWN world-generation identity through the vanilla {@code WorldInfo} + * API, because that is the channel a third-party {@code WorldType} reads when it identifies and + * configures itself — Advanced Rocketry cannot patch a foreign generator's read sites. + * + *

Vanilla's secondary-world {@code WorldInfo} answers this about the OVERWORLD: the getter + * delegates and the setter is an empty method, so a planet's own stamp used to be dropped in + * silence. The overworld's value is reported beside the planet's here to keep the assertion + * honest — the two must now DIFFER, which is only meaningful because both name a real type.

+ */ + @Test + public void planetPublishesItsOwnWorldTypeThroughWorldInfo() throws Exception { + int planet = firstTemplateArDimOrSkip(); + exec("artest dim load " + planet); + String info = exec("artest dim info " + planet); + + assertTrue("the dim must be loaded, or every field below is about a world that is not there: " + + info, info.contains("\"loaded\":true")); + assertTrue("this case is about a NATIVE planet: " + info, info.contains("\"terrainSource\":\"NATIVE\"")); + String published = field(info, "worldType"); + String overworld = field(info, "overworldWorldType"); + + // Both values must name something REAL before they are compared: two absences would read as + // agreement, and a comparison of two sources that are equal because neither exists cannot fail. + assertNamesAWorldType("worldType", published); + assertNamesAWorldType("overworldWorldType", overworld); + + assertEquals("a NATIVE planet generates with AR's own world type and must say so: " + info, + AR_PLANET_WORLD_TYPE, published); + assertFalse("the planet must no longer be answering with the SAVE's world type: " + info, + overworld.equals(published)); + } + + /** + * The generator-options channel, which is what makes third-party terrain more than decorative: + * a planet's chunk generator is configured from the planet's own settings string instead of the + * empty one a secondary world's {@code WorldInfo} used to hand out. + * + *

Asserted at three depths, because the first two alone would pass on a build where the + * string is published but never reaches the generator: the published value, the world type the + * dimension runs, and the BLOCKS on the ground. The preset below is deliberately absurd — three + * layers of diamond — so the last assertion cannot be satisfied by any default flat world.

+ */ + @Test + public void modWorldtypePlanetConfiguresItsForeignGeneratorFromItsOwnOptions() throws Exception { + int template = firstTemplateArDimOrSkip(); + String create = exec("artest worldgen create-terrain-dim " + + OPTIONS_DIM + " " + template + " MOD_WORLDTYPE flat " + FLAT_DIAMOND_PRESET); + assertTrue("create-terrain-dim must succeed: " + create, create.contains("\"ok\":true")); + + exec("artest dim load " + OPTIONS_DIM); + String info = exec("artest dim info " + OPTIONS_DIM); + + assertTrue("the dim must actually be running the foreign generator, else there is no " + + "options channel to measure: " + info, info.contains("ChunkGeneratorFlat")); + assertNamesAWorldType("worldType", field(info, "worldType")); + assertEquals("a MOD_WORLDTYPE planet must publish the foreign world type it actually runs: " + info, + "flat", field(info, "worldType")); + assertEquals("the planet must publish its OWN generator options: " + info, + FLAT_DIAMOND_PRESET, field(info, "generatorOptions")); + assertEquals("the save-global options string must be untouched — this is a per-dimension " + + "channel, not a write to the overworld: " + info, + "", field(info, "overworldGeneratorOptions")); + + // The player-visible half: the authored preset is what the generator actually built. + String stats = exec("artest worldgen ore-stats " + OPTIONS_DIM + " 0 0 2 minecraft:diamond_block"); + Matcher m = COUNT.matcher(stats); + assertTrue("ore-stats must report a count: " + stats, m.find()); + int count = Integer.parseInt(m.group(1)); + assertTrue("the authored flat preset must be the terrain that got generated; a default flat " + + "world has no diamond in it at all. count=" + count + " stats=" + stats, count > 0); + } + + /** A reported world-type name must be a real registered name, not an absence dressed as one. */ + private static void assertNamesAWorldType(String key, String value) { + assertFalse(key + " must name a world type, got the probe's null marker", "null".equals(value)); + assertFalse(key + " must name a world type, got an empty string", value.isEmpty()); + } + + /** Reads a flat string field out of a probe's JSON answer. */ + private static String field(String json, String key) { + Matcher m = Pattern.compile("\"" + key + "\":\"([^\"]*)\"").matcher(json); + assertTrue("probe answer has no string field '" + key + "': " + json, m.find()); + return m.group(1); + } + /** A registered non-overworld AR planet to clone, excluding the dims this test creates. */ private int firstTemplateArDimOrSkip() throws Exception { String joined = exec("artest dim list"); diff --git a/testframework/src/main/java/com/github/stannismod/forge/testing/client/ClientBot.java b/testframework/src/main/java/com/github/stannismod/forge/testing/client/ClientBot.java index 933e29504..1e483e518 100644 --- a/testframework/src/main/java/com/github/stannismod/forge/testing/client/ClientBot.java +++ b/testframework/src/main/java/com/github/stannismod/forge/testing/client/ClientBot.java @@ -191,6 +191,11 @@ public void pressEnterAfterTyping(String text) throws IOException { assertOk(execute(command)); } + /** + * The client's own view of itself: screen, GUI geometry, player position / health / held item, + * and — when a world is loaded — the {@code dimension} it renders and that world's + * {@code worldType} name, as the client learned it from the join/respawn packet. + */ public JsonObject reportState() throws IOException { return assertOk(execute(command("report_state"))); } diff --git a/testframework/src/main/java/com/github/stannismod/forge/testing/client/bridge/ForgeTestClientBootstrap.java b/testframework/src/main/java/com/github/stannismod/forge/testing/client/bridge/ForgeTestClientBootstrap.java index 8f0940fb3..bbb53e7d0 100644 --- a/testframework/src/main/java/com/github/stannismod/forge/testing/client/bridge/ForgeTestClientBootstrap.java +++ b/testframework/src/main/java/com/github/stannismod/forge/testing/client/bridge/ForgeTestClientBootstrap.java @@ -499,6 +499,16 @@ private static JsonObject handleCommand(JsonObject request) { response.addProperty("guiXSize", intField(containerScreen, "xSize")); response.addProperty("guiYSize", intField(containerScreen, "ySize")); } + if (mc.world != null) { + // What the CLIENT believes about the world it is in. The world type arrives + // in the join/respawn packet and is what client-side generator and terrain + // code identifies the world by, so a mod publishing it per dimension is only + // verifiable from here. + response.addProperty("dimension", mc.world.provider.getDimension()); + response.addProperty("worldType", + mc.world.getWorldInfo().getTerrainType() == null + ? "" : mc.world.getWorldInfo().getTerrainType().getName()); + } if (mc.player != null) { response.addProperty("selectedHotbar", mc.player.inventory.currentItem); response.addProperty("playerX", mc.player.posX); From d9a115adbfd9cad9c4531c5827163e97b1cb9d7a Mon Sep 17 00:00:00 2001 From: StannisMod Date: Tue, 11 Aug 2026 16:47:04 +0300 Subject: [PATCH 03/42] feat: derive planet properties, realize bodies, and grow the retinue - planet physics derived from (seed, cell); type is an XML preset - mass and radius primary, gravity derived as M/R^2 - a descent realizes a procedural body into a dimension, idempotently - long-tailed retinue with moons, belts, rings and distinct cells - 43 new unit tests and one server e2e --- .../command/test/TestProbeCommand.java | 155 ++++++ .../dimension/DimensionManager.java | 17 + .../dimension/DimensionProperties.java | 176 +++++- .../space/SystemBodiesProducer.java | 7 +- .../tile/TileAdvancedFlightComputer.java | 25 +- .../universe/BodyProfile.java | 155 ++++++ .../advancedRocketry/universe/CellHash.java | 59 ++ .../universe/ClusteredGalaxyGenerator.java | 264 +++++++-- .../universe/PlanetDerivation.java | 351 ++++++++++++ .../universe/PlanetRealizer.java | 260 +++++++++ .../universe/PlanetTypePreset.java | 284 ++++++++++ .../universe/PlanetTypes.java | 310 +++++++++++ .../advancedRocketry/universe/SystemBody.java | 53 +- .../universe/SystemContent.java | 12 +- .../universe/TerrainOption.java | 129 +++++ .../universe/UniverseRegistry.java | 78 +++ .../util/OreGenProperties.java | 64 +++ .../util/XMLPlanetLoader.java | 321 +++++++++-- .../world/provider/WorldProviderPlanet.java | 15 + .../ProceduralPlanetRealizationE2ETest.java | 173 ++++++ .../unit/ClusteredGalaxyGeneratorTest.java | 19 +- .../test/unit/PlanetDerivationTest.java | 508 ++++++++++++++++++ .../test/unit/PlanetRealizationTest.java | 248 +++++++++ .../test/unit/SystemRetinueTest.java | 340 ++++++++++++ 24 files changed, 3912 insertions(+), 111 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/BodyProfile.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/CellHash.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/PlanetTypePreset.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/TerrainOption.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/server/ProceduralPlanetRealizationE2ETest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 261fe9b48..0f785acbe 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -4099,6 +4099,161 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] send(sender, out.toString()); return; } + // gen-install [seed]: install a procedural + // galaxy generator and bind a seed. A world with no in its planetDefs runs the + // authored-anchors-only default, so without this there are no procedural systems to realize at + // all and every test about them would be a test about an empty universe. `gen-reset` puts the + // default back; a shared-server class MUST call it, because the generator is a JVM global. + if (args.length >= 5 && "gen-install".equalsIgnoreCase(args[0])) { + zmaster587.advancedRocketry.universe.UniverseRegistry reg = + zmaster587.advancedRocketry.universe.UniverseRegistry.get(server); + if (reg == null) { + send(sender, "{\"error\":\"registry unavailable\"}"); + return; + } + double density = parseDoubleOr(args[1], 0.9d); + int minSpacing = parseIntOr(args[2], 8); + int clusterScale = parseIntOr(args[3], 8); + double voidFraction = parseDoubleOr(args[4], 0d); + long seed = args.length >= 6 ? parseLongOr(args[5], 0L) : reg.worldSeed(); + zmaster587.advancedRocketry.universe.UniverseRegistry.setGenerator( + new zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator( + new zmaster587.advancedRocketry.universe.GalaxyGenConfig(density, minSpacing, + clusterScale, voidFraction, null))); + reg.bindWorldSeed(seed); + send(sender, "{\"ok\":true,\"seed\":" + seed + ",\"minSpacing\":" + minSpacing + "}"); + return; + } + if (args.length >= 1 && "gen-reset".equalsIgnoreCase(args[0])) { + zmaster587.advancedRocketry.universe.UniverseRegistry.setGenerator(null); + send(sender, "{\"ok\":true}"); + return; + } + // find-procedural : the first cell in a box around the origin that holds a body a ship + // could land on but that has NO dimension yet — the precondition of every realization test, and + // the thing that is impossible to write down as a literal because it depends on the seed. + if (args.length >= 2 && "find-procedural".equalsIgnoreCase(args[0])) { + zmaster587.advancedRocketry.universe.UniverseRegistry reg = + zmaster587.advancedRocketry.universe.UniverseRegistry.get(server); + if (reg == null) { + send(sender, "{\"error\":\"registry unavailable\"}"); + return; + } + long r = parseIntOr(args[1], 8); + for (long x = -r; x <= r; x++) { + for (long y = -r; y <= r; y++) { + for (long z = -r; z <= r; z++) { + zmaster587.advancedRocketry.space.GalacticCoord cell = + zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal(x, y, z, + 0L, 0L, 0L); + for (zmaster587.advancedRocketry.universe.SystemBody b : reg.bodiesAt(cell)) { + if (b.kind().canDescend() + && b.dimId() == zmaster587.advancedRocketry.api.Constants.INVALID_PLANET) { + send(sender, "{\"ok\":true,\"sx\":" + x + ",\"sy\":" + y + ",\"sz\":" + z + + ",\"cellKey\":\"" + cell.cellKey() + "\",\"kind\":\"" + b.kind() + + "\",\"orbitalDist\":" + b.orbitalDistance() + + ",\"starId\":" + b.starId() + "}"); + return; + } + } + } + } + } + send(sender, "{\"ok\":false,\"reason\":\"no unrealized landable body in range\"}"); + return; + } + // derived : what the DERIVATION says about the body in that cell, without + // realizing anything. This is the answer a telescope gives from across the system, and the whole + // point of it is that a landing has to agree with it — so a test compares this against the + // realized dimension's own properties rather than against a literal it wrote itself. + if (args.length >= 4 && "derived".equalsIgnoreCase(args[0])) { + zmaster587.advancedRocketry.universe.UniverseRegistry reg = + zmaster587.advancedRocketry.universe.UniverseRegistry.get(server); + if (reg == null) { + send(sender, "{\"error\":\"registry unavailable\"}"); + return; + } + zmaster587.advancedRocketry.space.GalacticCoord cell = + zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal( + parseIntOr(args[1], 0), parseIntOr(args[2], 0), parseIntOr(args[3], 0), + 0L, 0L, 0L); + java.util.Optional anchor = + reg.anchorForCell(cell); + java.util.Optional star = + reg.starAt(cell); + zmaster587.advancedRocketry.universe.SystemBody target = null; + int variant = 0; + int seen = 0; + for (zmaster587.advancedRocketry.universe.SystemBody b : reg.bodiesAt(cell)) { + if (b.kind() == zmaster587.advancedRocketry.universe.SystemBodyKind.STAR + || b.kind() == zmaster587.advancedRocketry.universe.SystemBodyKind.STATION_SLOT + || b.kind() == zmaster587.advancedRocketry.universe.SystemBodyKind.ASTEROID_BELT) { + continue; + } + if (target == null && b.kind().canDescend()) { + target = b; + variant = seen; + } + seen++; + } + if (target == null || !anchor.isPresent() || !star.isPresent()) { + send(sender, "{\"ok\":false,\"reason\":\"no landable body derivable at that cell\"}"); + return; + } + zmaster587.advancedRocketry.universe.BodyProfile p = + zmaster587.advancedRocketry.universe.PlanetDerivation.derive(reg.worldSeed(), + anchor.get(), target.name(), variant, star.get(), + target.kind() == zmaster587.advancedRocketry.universe.SystemBodyKind.MOON, + target.orbitalDistance()); + send(sender, "{\"ok\":true,\"type\":\"" + p.typeName() + "\",\"orbitalDist\":" + + p.orbitalDistance() + ",\"mass\":" + p.massEarths() + ",\"radius\":" + + p.radiusEarths() + ",\"gravity\":" + p.gravityPercent() + ",\"pressure\":" + + p.pressure() + ",\"temperature\":" + p.temperatureKelvin() + ",\"oxygen\":" + + p.hasOxygen() + ",\"locked\":" + p.tidallyLocked() + ",\"metallicity\":" + + p.metallicity() + ",\"terrainSource\":\"" + p.terrain().source() + "\"}"); + return; + } + // realize : mint the dimension for the landable body in that cell and report what + // the world it produced actually carries. The realization path a descent drives, called + // directly, so the properties can be compared with `derived` without flying anything. + if (args.length >= 4 && "realize".equalsIgnoreCase(args[0])) { + zmaster587.advancedRocketry.space.GalacticCoord cell = + zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal( + parseIntOr(args[1], 0), parseIntOr(args[2], 0), parseIntOr(args[3], 0), + 0L, 0L, 0L); + int dimId = zmaster587.advancedRocketry.universe.PlanetRealizer.realize(server, cell); + if (dimId == zmaster587.advancedRocketry.api.Constants.INVALID_PLANET) { + send(sender, "{\"ok\":false,\"reason\":\"nothing landable in that cell\"}"); + return; + } + zmaster587.advancedRocketry.dimension.DimensionProperties props = + zmaster587.advancedRocketry.dimension.DimensionManager.getInstance() + .getDimensionPropertiesOrNull(dimId); + if (props == null) { + send(sender, "{\"ok\":false,\"dim\":" + dimId + ",\"reason\":\"no properties registered\"}"); + return; + } + zmaster587.advancedRocketry.universe.UniverseRegistry reg = + zmaster587.advancedRocketry.universe.UniverseRegistry.get(server); + boolean descendTarget = false; + if (reg != null) { + for (zmaster587.advancedRocketry.universe.SystemBody b : reg.bodiesAt(cell)) { + if (b.dimId() == dimId && b.isDescendTarget()) { + descendTarget = true; + } + } + } + send(sender, "{\"ok\":true,\"dim\":" + dimId + ",\"name\":\"" + props.getName() + + "\",\"orbitalDist\":" + props.getOrbitalDist() + ",\"mass\":" + props.getMass() + + ",\"radius\":" + props.getRadius() + ",\"gravity\":" + + Math.round(props.getGravitationalMultiplier() * 100f) + ",\"pressure\":" + + props.getAtmosphereDensity() + ",\"temperature\":" + props.averageTemperature + + ",\"oxygen\":" + props.hasOxygen + ",\"locked\":" + props.isTidallyLocked() + + ",\"metallicity\":" + props.getMetallicity() + ",\"gasGiant\":" + + props.isGasGiant() + ",\"terrainSource\":\"" + props.getTerrainSource() + + "\",\"descendTarget\":" + descendTarget + ",\"starId\":" + props.getStarId() + "}"); + return; + } // find-afc : report a subspace block position + durable ship id of the settled ship in slot // , so a descent e2e can drive requestDescent for it. Located via the ledger coord (headless // the AFC does not tick, so the coord stays the settle coord) -> world pose -> the queryable ship diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java index 1890f7c68..8ffc6471c 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java @@ -384,6 +384,17 @@ public DimensionProperties generateRandomGasGiant(int starId, String name, int b properties.averageTemperature = AstronomicalBodyHelper.getAverageTemperature(properties.getStar(), properties.getSolarOrbitalDistance(), properties.getAtmosphereDensity()); properties.setGasGiant(true); + // Rings belong to giants, and on a giant they are the RULE rather than a flourish: all four of + // the Solar System's have them, because only a body that massive has a Roche limit reaching far + // enough past its own surface for a moon to have come apart out there. The rocky-planet path + // still rolls its rare 1-in-50; this is the same story told where it actually happens. + if (random.nextInt(4) != 0) { + properties.setHasRings(true); + properties.ringColor[0] = properties.skyColor[0]; + properties.ringColor[1] = properties.skyColor[1]; + properties.ringColor[2] = properties.skyColor[2]; + } + // Add all gasses for the default world for (FluidGasGiantGas gas : AdvancedRocketryFluids.getGasGiantGasses()) { if (((properties.gravitationalMultiplier * 100) >= gas.getMinGravity()) && (gas.getMaxGravity() >= (properties.gravitationalMultiplier * 100)) && 0 > (Math.random() - gas.getChance())) { @@ -1079,6 +1090,12 @@ public void createAndLoadDimensions(boolean resetFromXml) { ? null : new zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator(galaxyGenConfig)); + // Install the authored planet-type table for the same reason and on the same terms: it is a + // JVM-global, so an absent (or trimmed) section must restore the stock set rather + // than leave the previous world's presets standing. + zmaster587.advancedRocketry.universe.PlanetTypes.setPresets( + dimCouplingList == null ? null : dimCouplingList.planetTypes); + // make sure to set dim offset back to original to make things consistant DimensionManager.dimOffset = dimOffset; diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java index 4e73ab4d6..0547fd8f2 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java @@ -196,6 +196,54 @@ private static float clampFeatureFrequencyMultiplier(float multiplier) { * {@code WorldInfo} when it identifies itself; an empty string means "your defaults". */ private String terrainGeneratorOptions = ""; + + // ─── Bulk properties: mass and radius are PRIMARY, gravity is derived from them ──────────────── + /** + * This body's mass in Earth masses, or {@link #BULK_UNSET} when nothing has stated one. + * + *

Mass and radius are the PRIMARY bulk properties and surface gravity is what falls out of them + * ({@code g = M/R²}) — not the other way round. That ordering is what lets a scan advertise a + * planet's mass ({@code PlanetInfoField.MASS} is promised at telescope tier, for every planet, + * authored ones included) and what makes the zoning of a procedural system physical rather than + * tabulated: a big cold body accretes gas and becomes a giant, a small hot one cannot hold air.

+ * + *

{@link #gravitationalMultiplier} REMAINS an explicit override. A planet whose XML states a + * gravity keeps exactly that gravity, whatever its mass and radius say, so no authored world moves + * when this arrives; the derivation only fills in a gravity nobody stated.

+ */ + private double mass = BULK_UNSET; + /** This body's radius in Earth radii, or {@link #BULK_UNSET}. See {@link #mass}. */ + private double radius = BULK_UNSET; + /** + * Whether {@link #gravitationalMultiplier} was STATED rather than derived. The single bit that keeps + * "authored planets are unchanged" true: it is set by the XML element, by the public setter and by + * anything that assigns the field directly through the legacy path, and it makes + * {@link #setBulk} leave the gravity alone. + */ + private boolean gravityAuthored; + /** + * Whether this world keeps one face permanently to its star. + * + *

An explicit flag and not a {@code rotationalPeriod} of zero: zero is mapped back to a full day + * by the sleep arithmetic, so it would silently mean "an ordinary planet" — the one value that + * cannot express this. A locked world has no day/night cycle at all, which is a different statement + * from "its day is long".

+ */ + private boolean tidallyLocked; + /** + * The parent star's metal content relative to Sol, and therefore how metal-rich this world's ore is. + * + *

It scales the METALLIC entries of whatever ore palette this world's climate earns it; it does + * not decide which kinds of deposit are possible. Climate answers "what sort of deposits", the star + * answers "how much metal is in them", and the two multiply rather than compete.

+ */ + private double metallicity = 1d; + /** Lazily-built, never persisted: this world's own scaled copy of the shared climate ore table. */ + private transient OreGenProperties scaledOreCache; + private transient double scaledOreCacheFor = Double.NaN; + + /** Sentinel for {@link #mass} / {@link #radius}: nobody has stated one. */ + public static final double BULK_UNSET = 0d; //public int target_sea_level; // modId must be declared explicitly: this @SidedProxy lives outside the @Mod class, and the jar @@ -453,7 +501,19 @@ public Object clone() { public OreGenProperties getOreGenProperties(World world) { if (oreProperties != null) return oreProperties; - return OreGenProperties.getOresForPressure(AtmosphereTypes.getAtmosphereTypeFromValue(originalAtmosphereDensity), Temps.getTempFromValue(getAverageTemp())); + OreGenProperties climate = OreGenProperties.getOresForPressure( + AtmosphereTypes.getAtmosphereTypeFromValue(originalAtmosphereDensity), + Temps.getTempFromValue(getAverageTemp())); + if (climate == null || metallicity == 1d) + return climate; + // The climate table is a SHARED static object — one instance per (pressure, temperature) cell, + // handed to every world that lands in it — so a per-planet scaling must never mutate it. This + // world gets its own copy instead, cached because ore generation asks per chunk. + if (scaledOreCache == null || scaledOreCacheFor != metallicity) { + scaledOreCache = climate.withMetalsScaled(metallicity); + scaledOreCacheFor = metallicity; + } + return scaledOreCache; } /** @@ -487,6 +547,91 @@ public void resetProperties() { terrainTemplate = ""; terrainGeneratorOptions = ""; laserDrillOres = new ArrayList<>(); + mass = BULK_UNSET; + radius = BULK_UNSET; + gravityAuthored = false; + tidallyLocked = false; + metallicity = 1d; + scaledOreCache = null; + scaledOreCacheFor = Double.NaN; + } + + // ─── Bulk properties ─────────────────────────────────────────────────────── + + /** This body's mass in Earth masses, or {@link #BULK_UNSET} when nobody has stated one. */ + public double getMass() { + return mass; + } + + /** This body's radius in Earth radii, or {@link #BULK_UNSET}. */ + public double getRadius() { + return radius; + } + + public boolean hasBulkProperties() { + return mass > BULK_UNSET && radius > BULK_UNSET; + } + + /** + * State this body's mass and radius, deriving surface gravity from them unless a gravity was + * explicitly authored. + * + * @param massEarths mass in Earth masses + * @param radiusEarths radius in Earth radii + */ + public void setBulk(double massEarths, double radiusEarths) { + this.mass = Math.max(0d, massEarths); + this.radius = Math.max(0d, radiusEarths); + if (!gravityAuthored && hasBulkProperties()) { + gravitationalMultiplier = (float) derivedGravity(this.mass, this.radius); + } + } + + /** + * Surface gravity in Earth gravities from mass and radius — {@code g = M/R²} — clamped to the range + * the game can actually run a player in. The floor is the same one the legacy random generator has + * always used; the ceiling is {@link #MAX_GRAVITY}. + */ + public static double derivedGravity(double massEarths, double radiusEarths) { + double g = massEarths / Math.max(1e-6d, radiusEarths * radiusEarths); + double lo = 0.05d; + double hi = MAX_GRAVITY / 100d; + if (Double.isNaN(g) || g < lo) { + return lo; + } + return g > hi ? hi : g; + } + + /** Whether a gravity was STATED for this body rather than derived from its bulk. */ + public boolean isGravityAuthored() { + return gravityAuthored; + } + + /** Mark this body's {@link #gravitationalMultiplier} as authored — the XML/override path. */ + public void setGravityAuthored(boolean authored) { + this.gravityAuthored = authored; + } + + /** + * Whether this world keeps one face to its star: no day/night cycle at all, rather than a long day. + */ + public boolean isTidallyLocked() { + return tidallyLocked; + } + + public void setTidallyLocked(boolean locked) { + this.tidallyLocked = locked; + } + + /** The parent star's metal content relative to Sol — see {@link #metallicity}. */ + public double getMetallicity() { + return metallicity; + } + + public void setMetallicity(double value) { + this.metallicity = (Double.isNaN(value) || value <= 0d) ? 1d : value; + this.scaledOreCache = null; + this.scaledOreCacheFor = Double.NaN; } public List getHarvestableGasses() { @@ -505,6 +650,9 @@ public float getGravitationalMultiplier() { @Override public void setGravitationalMultiplier(float mult) { gravitationalMultiplier = mult; + // Stating a gravity is what makes it an override: from here on the mass/radius derivation must + // not touch it, or an authored planet would silently change the moment it gained a mass. + gravityAuthored = true; } public List getSpawnListEntries() { @@ -1687,6 +1835,15 @@ else if (nbt.hasKey("biomes", NBT.TAG_INT_ARRAY)) { } gravitationalMultiplier = nbt.getFloat("gravitationalMultiplier"); + // Bulk properties, written only when stated: an absent key leaves the sentinel, so a world + // saved before planets had a mass reloads with exactly the gravity it already had. + mass = nbt.hasKey("mass") ? nbt.getDouble("mass") : BULK_UNSET; + radius = nbt.hasKey("radius") ? nbt.getDouble("radius") : BULK_UNSET; + gravityAuthored = nbt.getBoolean("gravityAuthored"); + tidallyLocked = nbt.getBoolean("tidallyLocked"); + metallicity = nbt.hasKey("metallicity") ? nbt.getDouble("metallicity") : 1d; + scaledOreCache = null; + scaledOreCacheFor = Double.NaN; orbitalDist = nbt.getInteger("orbitalDist"); orbitTheta = nbt.getDouble("orbitTheta"); baseOrbitTheta = nbt.getDouble("baseOrbitTheta"); @@ -2066,6 +2223,23 @@ public void writeToNBT(NBTTagCompound nbt) { nbt.setInteger("starId", starId); nbt.setFloat("gravitationalMultiplier", gravitationalMultiplier); + // Non-default-only, the terrainSource idiom: a planet that never stated a mass writes no mass + // key, so its NBT stays byte-identical to what it wrote before bulk properties existed. + if (mass > BULK_UNSET) { + nbt.setDouble("mass", mass); + } + if (radius > BULK_UNSET) { + nbt.setDouble("radius", radius); + } + if (gravityAuthored) { + nbt.setBoolean("gravityAuthored", true); + } + if (tidallyLocked) { + nbt.setBoolean("tidallyLocked", true); + } + if (metallicity != 1d) { + nbt.setDouble("metallicity", metallicity); + } nbt.setInteger("orbitalDist", orbitalDist); nbt.setDouble("orbitTheta", orbitTheta); nbt.setDouble("baseOrbitTheta", baseOrbitTheta); diff --git a/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java b/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java index 56c054d8d..0aaf3fad3 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java +++ b/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java @@ -119,8 +119,13 @@ public static Map> buildByDim(Map loa if (found != null) { for (SystemBody b : found) { BlockDelta dir = b.absoluteAt(worldTick).minus(observer); + // "Can a ship land here", not "does a world already exist". A procedural planet has + // no dimension until a descent mints one, so highlighting only realized bodies + // would hide the descent boundary of every world nobody has visited — which is + // precisely the set a pilot is out there looking for. The flag is a render hint; + // the logic that needs a real dimension still asks isDescendTarget(). bodies.add(new RenderBody(b.kind().ordinal(), dir.dx(), dir.dy(), dir.dz(), - renderDimIdOf(b), b.isDescendTarget())); + renderDimIdOf(b), b.kind().canDescend())); } } byDim.put(slotDim, bodies); diff --git a/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java b/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java index f82722bb5..37b919db8 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java @@ -417,7 +417,11 @@ public void update() { zmaster587.advancedRocketry.space.GalacticCoord shipCoord = settled.coord; long radius = zmaster587.advancedRocketry.space.ShipEntryController.DESCENT_RADIUS_BLOCKS; for (zmaster587.advancedRocketry.universe.SystemBody body : reg.bodiesAt(shipCoord)) { - if (!body.isDescendTarget()) { + // A procedural body has no dimension until somebody flies down to it, so the + // filter here is "can this be landed on", not "does it already have a world". + // The world is minted below, once the ship is genuinely close enough to + // descend — a scan must never allocate a dimension. + if (!body.kind().canDescend()) { continue; } // Ship and body are in the SAME cell here (bodiesAt filters by name), so @@ -427,10 +431,21 @@ public void update() { double distance = Math.sqrt(shipCoord.staticFrameDistanceSqTo( body.addressAt(zmaster587.advancedRocketry.space.SpaceSubsystem .spaceClock()))); - if (zmaster587.advancedRocketry.space.DescentController - .shouldTriggerDescent(true, true, distance, radius) - && descentCtl.requestDescent(world.provider.getDimension(), - getPos(), shipId, body.dimId())) { + if (!zmaster587.advancedRocketry.space.DescentController + .shouldTriggerDescent(true, true, distance, radius)) { + continue; + } + int targetDim = body.dimId(); + if (targetDim == zmaster587.advancedRocketry.api.Constants.INVALID_PLANET) { + targetDim = zmaster587.advancedRocketry.universe.PlanetRealizer + .realize(server, body.name()); + if (targetDim + == zmaster587.advancedRocketry.api.Constants.INVALID_PLANET) { + continue; // nothing landable here after all + } + } + if (descentCtl.requestDescent(world.provider.getDimension(), + getPos(), shipId, targetDim)) { // The crossing started: this tile was cut out of the slot world - stop // publishing from a stale tick. The re-assembled ship resumes planet-side. return; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/BodyProfile.java b/src/main/java/zmaster587/advancedRocketry/universe/BodyProfile.java new file mode 100644 index 000000000..a731630d3 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/BodyProfile.java @@ -0,0 +1,155 @@ +package zmaster587.advancedRocketry.universe; + +/** + * Everything a procedural body IS, derived from {@code (seed, cell)} alone — the object that crosses + * the universe→dimension layer boundary. + * + *

The UNIVERSE layer produces one of these ({@link PlanetDerivation}); the DIMENSION layer consumes + * it when a body is realized into a real world. Nothing here is a world, a block, a biome or a + * dimension id: a profile can be computed for a body nobody has ever visited, which is precisely what + * lets a telescope report a world's mass, atmosphere and temperature from across the system while the + * player still has no suit for it.

+ * + *

Determinism is the contract, not an optimisation. The scan and the landing must describe + * the same world, so realization MATERIALIZES this profile rather than rolling fresh values. Terrain is + * the one field a scan does not promise from afar ({@code TERRAIN_TYPE} sits at the approach tier), and + * it is still derived here so that everything about a body has exactly one origin.

+ * + *

Immutable value object.

+ */ +public final class BodyProfile { + + private final SystemBodyKind kind; + private final String typeName; + private final PlanetTypePreset preset; + private final int orbitalDistance; + private final double massEarths; + private final double radiusEarths; + private final int gravityPercent; + private final int pressure; + private final int temperatureKelvin; + private final boolean hasOxygen; + private final boolean tidallyLocked; + private final boolean hasRings; + private final double metallicity; + private final TerrainOption terrain; + + public BodyProfile(SystemBodyKind kind, String typeName, PlanetTypePreset preset, int orbitalDistance, + double massEarths, double radiusEarths, int gravityPercent, int pressure, + int temperatureKelvin, boolean hasOxygen, boolean tidallyLocked, boolean hasRings, + double metallicity, TerrainOption terrain) { + this.kind = kind; + this.typeName = typeName; + this.preset = preset; + this.orbitalDistance = orbitalDistance; + this.massEarths = massEarths; + this.radiusEarths = radiusEarths; + this.gravityPercent = gravityPercent; + this.pressure = pressure; + this.temperatureKelvin = temperatureKelvin; + this.hasOxygen = hasOxygen; + this.tidallyLocked = tidallyLocked; + this.hasRings = hasRings; + this.metallicity = metallicity; + this.terrain = terrain; + } + + /** What this body is as an addressable object — planet, giant, moon or belt. */ + public SystemBodyKind kind() { + return kind; + } + + /** The planet type's name, or {@link PlanetTypes#UNCLASSIFIED} when no preset admitted this world. */ + public String typeName() { + return typeName; + } + + /** The admitting preset, or {@code null} when none did. */ + public PlanetTypePreset preset() { + return preset; + } + + /** Orbital radius in Advanced Rocketry distance units (100 = 1 AU). */ + public int orbitalDistance() { + return orbitalDistance; + } + + /** Mass in Earth masses — PRIMARY, not derived from gravity. */ + public double massEarths() { + return massEarths; + } + + /** Radius in Earth radii — PRIMARY. */ + public double radiusEarths() { + return radiusEarths; + } + + /** Surface gravity in percent of Earth's, derived as {@code M/R²} and clamped to the game's range. */ + public int gravityPercent() { + return gravityPercent; + } + + /** Surface pressure in atmosphere-density units (100 = 1 atm). */ + public int pressure() { + return pressure; + } + + /** Surface temperature in Kelvin, computed WITH the derived atmosphere. */ + public int temperatureKelvin() { + return temperatureKelvin; + } + + /** Whether the atmosphere is breathable — an independent rare roll, never a consequence of the rest. */ + public boolean hasOxygen() { + return hasOxygen; + } + + /** + * Whether this world keeps one face to its star: permanent day, permanent night, and a habitable + * terminator strip between them as the only temperate ground. + */ + public boolean tidallyLocked() { + return tidallyLocked; + } + + /** + * Whether this body wears a ring system. + * + *

Rings are where the "something was torn apart" story actually lives: a moon that wandered + * inside its planet's Roche limit came apart into a disc, and only a body massive enough for that + * limit to reach beyond its own surface can hold the result. Every one of the Solar System's four + * giants has rings, so on a giant this is COMMON rather than a rare flourish; on a rocky world it + * effectively never happens.

+ */ + public boolean hasRings() { + return hasRings; + } + + /** + * The parent star's metal content, relative to Sol. A metal-poor star formed a metal-poor disk, so + * this scales the METAL fraction of whatever ore palette the world's climate earns it — it does not + * change which kinds of deposit are possible. + */ + public double metallicity() { + return metallicity; + } + + /** How this world's terrain is generated, drawn from its type's weighted list. */ + public TerrainOption terrain() { + return terrain; + } + + /** Whether this body can be stood on at all — the giants cannot. */ + public boolean hasSurface() { + return kind != SystemBodyKind.GAS_GIANT && kind != SystemBodyKind.ASTEROID_BELT + && kind != SystemBodyKind.STAR; + } + + @Override + public String toString() { + return "BodyProfile[" + kind + " " + typeName + " d=" + orbitalDistance + " M=" + massEarths + + " R=" + radiusEarths + " g=" + gravityPercent + "% p=" + pressure + " T=" + + temperatureKelvin + "K" + (hasOxygen ? " O2" : "") + (tidallyLocked ? " locked" : "") + + (hasRings ? " rings" : "") + " Z=" + metallicity + " " + terrain + ']'; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/CellHash.java b/src/main/java/zmaster587/advancedRocketry/universe/CellHash.java new file mode 100644 index 000000000..b91e8e61e --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/CellHash.java @@ -0,0 +1,59 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.space.GalacticCoord; + +/** + * The one mixer every procedural answer about a cell is drawn from. + * + *

A splitmix-style mix of the world seed, an integer coordinate triple and a per-field salt, uniform + * over 64 bits. Distinct salts are what keep the independent draws — blob mask, occupancy, star type, + * body count, a planet's radius — from correlating with each other.

+ * + *

This arithmetic is a save-compatibility surface for the LIFE of a world, not an implementation + * detail. Every unpinned procedural system is re-derived from it on every query, so changing a + * constant here silently moves stars and reshapes planets in every existing save that has not been + * touched. It lives in one place for exactly that reason: two copies of a mixer are two things to + * forget about.

+ */ +final class CellHash { + + private CellHash() { + } + + /** Mix {@code seed}, the triple {@code (a,b,c)} and {@code salt} into a uniform 64-bit value. */ + static long of(long seed, long a, long b, long c, long salt) { + long h = seed + salt * 0x9E3779B97F4A7C15L; + h ^= a; + h *= 0xFF51AFD7ED558CCDL; + h ^= h >>> 33; + h ^= b; + h *= 0xC4CEB9FE1A85EC53L; + h ^= h >>> 33; + h ^= c; + h *= 0xFF51AFD7ED558CCDL; + h ^= h >>> 33; + return h; + } + + /** Mix a cell's own field draw. */ + static long ofCell(long seed, GalacticCoord cell, long salt) { + return of(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ(), salt); + } + + /** + * Mix a per-BODY field draw inside a cell's system. + * + *

The body index is XORed into the seed through a different multiplier than {@link #of} uses for + * the salt, so the two cannot merge into {@code (i + salt) * G} and correlate neighbouring bodies' + * draws — which would make body {@code i}'s radius a near-copy of body {@code i+1}'s.

+ */ + static long ofBody(long seed, GalacticCoord cell, int index, long salt) { + return of(seed ^ (index * 0xD1B54A32D192ED03L), cell.sectorX(), cell.sectorY(), cell.sectorZ(), + salt); + } + + /** Map a 64-bit hash to a double in {@code [0, 1)}. */ + static double norm(long h) { + return (h >>> 11) * 0x1.0p-53; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 639a4e218..2631cc968 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -3,13 +3,16 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import zmaster587.advancedRocketry.api.Constants; import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; /** * A deterministic, addon-default {@link IGalaxyGenerator} producing a CLUSTERED procedural galaxy @@ -51,10 +54,57 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { // by the super-cell partition (minSpacing/2 - margin) so two systems' neighbourhoods never interleave. private static final long SALT_BODYCOUNT = 0x11L; private static final long SALT_BODYANG = 0x12L; - private static final long SALT_BODYRAD = 0x13L; + // 0x13 was SALT_BODYRAD, the uniform cell-radius draw. Retired: a body's cell radius now FOLLOWS + // its orbital distance (PlanetDerivation.orbitFraction), so the two layouts cannot disagree. The + // number stays burned so a future draw cannot silently inherit an old galaxy's stream. private static final long SALT_BODYY = 0x14L; - private static final long SALT_BELT = 0x15L; - private static final int MAX_PROC_PLANETS = 6; + // 0x15 was SALT_BELT, the "roughly a third of systems end in a belt" roll. Retired: an outer belt is + // now MANDATORY and an inner one is derived from a giant, so a belt is never a coin toss. The number + // stays burned so a future draw cannot inherit an old galaxy's stream. + private static final long SALT_MOONCOUNT = 0x16L; + private static final long SALT_MOONANG = 0x17L; + private static final long SALT_MOONRAD = 0x18L; + + // ─── The retinue: how many bodies a system has, and where they sit ───────── + // Every number here is a balance knob. What is NOT a knob is the shape: a long tail, a mandatory + // outer belt, and moons on the bodies big enough to hold them. + + /** + * Body count is drawn from a shifted exponential: a median around five or six, and a thin tail that + * occasionally produces a system of fifteen or more. A rich system is itself a find, which is what + * makes exploring for one worth doing — a fixed ceiling of six made every system the same size. + */ + private static final int MIN_PROC_PLANETS = 3; + private static final double PLANET_COUNT_SCALE = 3.385d; + /** + * Hard ceiling on the retinue. Not a balance number: {@code bodiesFor} runs on EVERY registry query + * — the render feed, the console's forecast, every proximity check — so the tail has to be bounded + * by something other than luck. + */ + private static final int MAX_PROC_PLANETS = 24; + + /** Moons per body, drawn as {@code floor(u^BIAS · (MAX+1))}: most bodies have none, giants have several. */ + private static final int MAX_MOONS_ROCKY = 2; + private static final int MAX_MOONS_GIANT = 5; + private static final double MOON_COUNT_BIAS = 1.9d; + /** A moon's orbit about its parent, in the parent-relative units the moon ephemeris is written in. */ + private static final int MOON_MIN_ORBIT = 20; + private static final int MOON_ORBIT_SPAN = 110; + + /** The outer belt sits this far beyond the outermost major body — the Kuiper analogue. */ + private static final double OUTER_BELT_FACTOR = 1.6d; + /** + * An inner belt sits at the resonance-cleared gap inside a giant. A belt is not a destroyed planet: + * it is material that never accreted because a nearby giant pumped relative velocities past the + * point where collisions stick — so a belt is DERIVED from a giant, and a system with no giant has + * no inner belt. + */ + private static final double INNER_BELT_RESONANCE = 1.8d; + + /** Deterministic angular step used when a body's first-choice cell is already occupied. */ + private static final double NUDGE_ANGLE = 2.399963229728653d; // the golden angle, in radians + /** How many relocations a body gets before its system is declared full. */ + private static final int NUDGE_ATTEMPTS = 96; /** Neighbourhood margin (cells) kept clear of the super-cell boundary. */ private static final int NEIGHBOURHOOD_MARGIN_CELLS = 2; /** Thin-disk half-thickness as a fraction of the orbit radius (bodies keep honest 3D Y — A#1a e1). */ @@ -139,6 +189,7 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { return Collections.emptyList(); } int starId = sys.get().starId(); + StellarBody star = sys.get().star(); List bodies = new ArrayList<>(); // The star sits at the anchor cell's centre. bodies.add(new SystemBody(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, starId)); @@ -148,31 +199,164 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { // face), so a radius <= 3s/8 - margin keeps every body inside the anchor's super-cell — member-cell // attribution by floorDiv stays exact. (The per-body box clamp below covers the tiny-spacing floor.) long s = config.minSpacing; + + // AT MOST ONE REAL BODY PER CELL, moons excepted. The draw picks each body's angle and radius + // independently, so two of them CAN land on the same cell — and two real bodies in one cell are + // two destinations a player can neither tell apart nor choose between. Claiming cells as they + // are used, and relocating a body that finds its first choice taken, is what keeps the + // generator's own output out of that state; the audit that reports it would otherwise fire on + // the generator itself, and the more bodies a system has the likelier that becomes. + Set taken = new HashSet<>(); + taken.add(cell.cellKey()); + + int count = retinueSize(seed, cell); + int outermostOrbit = 0; + int innermostGiantOrbit = 0; + for (int i = 0; i < count; i++) { + // The ORBIT is drawn first and the cell radius follows it, rather than the other way round: + // a body's physics is derived from its orbit, so letting the placement pick the radius would + // make every world's climate a function of the layout arithmetic. + int orbit = PlanetDerivation.orbitalDistanceOf(seed, cell, i, count, star); + GalacticCoord addr = placeBody(seed, cell, i, orbit, star, s, taken); + if (addr == null) { + continue; // this system's neighbourhood is full — a bound of the layout, not a failure + } + // Planet or giant is not a roll of its own: it falls out of the body's derived physics, + // which is what makes the zoning (rock inside, giants past the snow line) emerge instead + // of being authored. Kept here rather than at realization because the nav list, the sky + // and the descent trigger all read the kind long before anyone lands. + BodyProfile profile = PlanetDerivation.derive(seed, cell, addr, 0, star, false, orbit); + // Procedural bodies have no realized dimension yet — a descent (Layer 2) realizes one. + bodies.add(new SystemBody(addr, profile.kind(), Constants.INVALID_PLANET, starId, orbit)); + outermostOrbit = Math.max(outermostOrbit, orbit); + if (profile.kind() == SystemBodyKind.GAS_GIANT + && (innermostGiantOrbit == 0 || orbit < innermostGiantOrbit)) { + innermostGiantOrbit = orbit; + } + addMoons(bodies, seed, cell, addr, orbit, star, starId, profile); + } + + // An inner belt is DERIVED from a giant and never rolled: it is material a giant's resonances + // stopped from accreting, so it belongs in the gap inside one and a system with no giant has none. + if (innermostGiantOrbit > 0) { + addBelt(bodies, seed, cell, (int) (innermostGiantOrbit / INNER_BELT_RESONANCE), star, s, + starId, taken, count + 1); + } + // The outer belt is MANDATORY on every system — the Kuiper analogue, and the reason every system + // is worth arriving in: it is a gravity-well-free mining site that needs no landing, so a ship + // that drifts into any system at all has something to work. + int outerBelt = (int) Math.max(outermostOrbit * OUTER_BELT_FACTOR, + PlanetDerivation.innerOrbit(star) * 2d); + addBelt(bodies, seed, cell, outerBelt, star, s, starId, taken, count + 2); + return bodies; + } + + /** + * How many major bodies a system has. A shifted exponential: most systems are ordinary, a few are + * enormous, and the ceiling exists to bound the per-query cost rather than the fiction. + */ + public static int retinueSize(long seed, GalacticCoord anchor) { + double u = CellHash.norm(CellHash.ofCell(seed, anchor, SALT_BODYCOUNT)); + double tail = -Math.log(Math.max(1e-12d, 1d - u)) * PLANET_COUNT_SCALE; + int n = MIN_PROC_PLANETS + (int) tail; + return Math.max(1, Math.min(MAX_PROC_PLANETS, n)); + } + + /** + * Claim a free cell for a body orbiting at {@code orbit}, or {@code null} when the neighbourhood has + * no room left. + * + *

The first choice puts the body at the cell radius its orbit maps to, at a drawn angle. If that + * cell is already spoken for, the body is walked around the ring by the golden angle — which keeps + * its radius, and therefore keeps the system's cell layout in the same order as its orbits — and + * only then allowed to drift outward. A body that still finds nothing is dropped: a neighbourhood + * holds what it holds, and inventing a second occupant for a cell is the one outcome that is worse + * than a smaller system.

+ */ + private static GalacticCoord placeBody(long seed, GalacticCoord anchor, int index, int orbit, + StellarBody star, long s, Set taken) { long maxRadiusCells = Math.max(1L, 3L * s / 8L - NEIGHBOURHOOD_MARGIN_CELLS); double maxRadiusBlocks = (double) maxRadiusCells * GalacticCoord.CELL; double minRadiusBlocks = GalacticCoord.CELL; + double baseAngle = CellHash.norm(CellHash.ofBody(seed, anchor, index, SALT_BODYANG)) * 2d * Math.PI; + double baseRadius = minRadiusBlocks + PlanetDerivation.orbitFraction(orbit, star) + * Math.max(0d, maxRadiusBlocks - minRadiusBlocks); + double heightFraction = CellHash.norm(CellHash.ofBody(seed, anchor, index, SALT_BODYY)) - 0.5d; - int count = 1 + (int) Math.floorMod( - hash(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ(), SALT_BODYCOUNT), MAX_PROC_PLANETS); - for (int i = 0; i < count; i++) { - double angle = norm(hashBody(seed, cell, i, SALT_BODYANG)) * 2d * Math.PI; - double radius = minRadiusBlocks - + norm(hashBody(seed, cell, i, SALT_BODYRAD)) * Math.max(0d, maxRadiusBlocks - minRadiusBlocks); + for (int attempt = 0; attempt < NUDGE_ATTEMPTS; attempt++) { + double angle = baseAngle + attempt * NUDGE_ANGLE; + // Radius is held for a full turn of the ring before it is allowed to grow, so a relocation + // costs the body its angle long before it costs it its place in the orbital order. + double radius = Math.min(maxRadiusBlocks, baseRadius * (1d + 0.06d * (attempt / 16))); long lx = (long) (radius * Math.cos(angle)); long lz = (long) (radius * Math.sin(angle)); - long ly = (long) ((norm(hashBody(seed, cell, i, SALT_BODYY)) - 0.5d) * radius * PROC_DISK_FRACTION); + long ly = (long) (heightFraction * radius * PROC_DISK_FRACTION); // The body's address is its OWN cell's centre (zone content sits near the cell centre — A#1a), // box-clamped into the anchor's super-cell so member attribution stays exact at ANY minSpacing // (at tiny spacings the floor above can otherwise push a body across the super-cell face). - GalacticCoord addr = clampIntoSuperCell(cell.plusLocal(lx, ly, lz).cellCentre(), cell, s); - // Roughly a third of systems' outermost body is an asteroid belt rather than a planet. - SystemBodyKind kind = (i == count - 1 && norm(hashBody(seed, cell, i, SALT_BELT)) < 0.3d) - ? SystemBodyKind.ASTEROID_BELT - : SystemBodyKind.PLANET; - // Procedural bodies have no realized dimension yet — a descent (Layer 2) realizes one. - bodies.add(new SystemBody(addr, kind, Constants.INVALID_PLANET, starId)); + GalacticCoord addr = clampIntoSuperCell(anchor.plusLocal(lx, ly, lz).cellCentre(), anchor, s); + if (taken.add(addr.cellKey())) { + return addr; + } + } + return null; + } + + /** Append an asteroid belt at {@code orbit}, if the neighbourhood still has a cell for one. */ + private static void addBelt(List bodies, long seed, GalacticCoord anchor, int orbit, + StellarBody star, long s, int starId, Set taken, int index) { + int clamped = Math.max(1, orbit); + GalacticCoord addr = placeBody(seed, anchor, index, clamped, star, s, taken); + if (addr != null) { + bodies.add(new SystemBody(addr, SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, + starId, clamped)); } - return bodies; + } + + /** + * Append this body's moons. They share their parent's CELL by construction — a planet and its moons + * are one destination, which is the whole reason the one-real-body-per-cell invariant exempts them — + * and each carries its own live offset inside it. + * + *

Their {@code orbitalDistance} is the PARENT's distance from the star, not their own distance + * from the parent: that field is what a moon's climate is derived from, and what warms a moon is + * where its planet is. How far the moon sits from the planet lives in its ephemeris, which is the + * thing that actually positions it.

+ */ + private void addMoons(List bodies, long seed, GalacticCoord anchor, GalacticCoord parent, + int parentOrbit, StellarBody star, int starId, BodyProfile parentProfile) { + boolean giant = parentProfile.kind() == SystemBodyKind.GAS_GIANT; + int max = giant ? MAX_MOONS_GIANT : MAX_MOONS_ROCKY; + double u = CellHash.norm(CellHash.ofCell(seed, parent, SALT_MOONCOUNT)); + int moons = (int) (Math.pow(u, MOON_COUNT_BIAS) * (max + 1)); + if (moons > max) { + moons = max; + } + double parentGravity = Math.max(0.05d, parentProfile.gravityPercent() / 100d); + for (int j = 1; j <= moons; j++) { + int moonOrbit = MOON_MIN_ORBIT + (int) (CellHash.norm( + CellHash.ofBody(seed, parent, j, SALT_MOONRAD)) * MOON_ORBIT_SPAN); + double theta = CellHash.norm(CellHash.ofBody(seed, parent, j, SALT_MOONANG)) * 2d * Math.PI; + double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY + * AstronomicalBodyHelper.getMoonOrbitalPeriod(moonOrbit, (float) parentGravity); + BodyEphemeris law = BodyEphemeris.orbit(moonOrbit, theta, 0d, false, periodTicks, + SystemContent.MOON_UNIT_BLOCKS); + bodies.add(new SystemBody(parent, CellFrame.staticAt(parent), law, SystemBodyKind.MOON, + Constants.INVALID_PLANET, starId, parentOrbit)); + } + } + + /** + * The full derived profile of one of this generator's bodies — what realization materializes. + * + *

Answerable for a body nobody has visited, because it is the same pure derivation the kind above + * came from. The body carries its own orbit, so this stays correct for a PINNED system whose layout + * the live generator would no longer reproduce.

+ */ + public BodyProfile profileOf(long seed, GalacticCoord anchor, SystemBody body, StellarBody star, + int variant) { + return PlanetDerivation.derive(seed, anchor.cellCentre(), body.name(), variant, star, + body.kind() == SystemBodyKind.MOON, body.orbitalDistance()); } @Override @@ -211,22 +395,16 @@ private static long clampAxis(long sector, long anchorSector, long s, long margi return sector > hi ? hi : sector; } - private static long hashBody(long seed, GalacticCoord cell, int i, long field) { - // XOR the body index in with a DIFFERENT multiplier than hash() uses for the field salt, so the two - // don't merge into (i + field)*G and correlate neighbouring bodies' draws. - return hash(seed ^ (i * 0xD1B54A32D192ED03L), cell.sectorX(), cell.sectorY(), cell.sectorZ(), field); - } - /** The single system a super-cell hosts (its cell coordinate + fabricated system), or empty. */ private Optional systemForSuperCell(long seed, long supX, long supY, long supZ) { long cs = config.clusterScale; // Void mask: a super-cell whose blob is below the void fraction hosts nothing. - double blob = norm(hash(seed, Math.floorDiv(supX, cs), Math.floorDiv(supY, cs), Math.floorDiv(supZ, cs), - SALT_BLOB)); + double blob = CellHash.norm(CellHash.of(seed, Math.floorDiv(supX, cs), Math.floorDiv(supY, cs), + Math.floorDiv(supZ, cs), SALT_BLOB)); if (blob < config.voidFraction) { return Optional.empty(); } - if (norm(hash(seed, supX, supY, supZ, SALT_OCC)) >= config.density) { + if (CellHash.norm(CellHash.of(seed, supX, supY, supZ, SALT_OCC)) >= config.density) { return Optional.empty(); } long s = config.minSpacing; @@ -235,17 +413,17 @@ private Optional systemForSuperCell(long seed, long supX, long supY, // neighbouring super-cell (A#1a attribution guarantee). long band = Math.max(1L, s / 4L); long base = 3L * s / 8L; - long ox = base + Math.floorMod(hash(seed, supX, supY, supZ, SALT_OX), band); - long oy = base + Math.floorMod(hash(seed, supX, supY, supZ, SALT_OY), band); - long oz = base + Math.floorMod(hash(seed, supX, supY, supZ, SALT_OZ), band); + long ox = base + Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_OX), band); + long oy = base + Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_OY), band); + long oz = base + Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_OZ), band); GalacticCoord cell = GalacticCoord.ofSectorLocal(supX * s + ox, supY * s + oy, supZ * s + oz, 0L, 0L, 0L); return Optional.of(new Generated(cell, fabricate(seed, supX, supY, supZ))); } private StarSystem fabricate(long seed, long supX, long supY, long supZ) { - GalaxyGenConfig.StarType type = pickType(hash(seed, supX, supY, supZ, SALT_TYPE)); - double sizeFrac = norm(hash(seed, supX, supY, supZ, SALT_SIZE)); + GalaxyGenConfig.StarType type = pickType(CellHash.of(seed, supX, supY, supZ, SALT_TYPE)); + double sizeFrac = CellHash.norm(CellHash.of(seed, supX, supY, supZ, SALT_SIZE)); StellarBody star = new StellarBody(); star.setTemperature(type.temperature); @@ -269,27 +447,7 @@ private GalaxyGenConfig.StarType pickType(long h) { } private static int syntheticId(long seed, long supX, long supY, long supZ) { - return -(1 + (int) Math.floorMod(hash(seed, supX, supY, supZ, SALT_ID), SYNTHETIC_ID_RANGE)); - } - - /** A splitmix-style mix of the seed, an integer coordinate triple, and a salt. Uniform over 64 bits. */ - private static long hash(long seed, long a, long b, long c, long salt) { - long h = seed + salt * 0x9E3779B97F4A7C15L; - h ^= a; - h *= 0xFF51AFD7ED558CCDL; - h ^= h >>> 33; - h ^= b; - h *= 0xC4CEB9FE1A85EC53L; - h ^= h >>> 33; - h ^= c; - h *= 0xFF51AFD7ED558CCDL; - h ^= h >>> 33; - return h; - } - - /** Map a 64-bit hash to a double in {@code [0, 1)}. */ - private static double norm(long h) { - return (h >>> 11) * 0x1.0p-53; + return -(1 + (int) Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_ID), SYNTHETIC_ID_RANGE)); } private static final class Generated { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java new file mode 100644 index 000000000..a0a97654b --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java @@ -0,0 +1,351 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.dimension.DimensionProperties; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; + +/** + * Where a procedural body's PHYSICS comes from, and therefore where its TYPE comes from. + * + *

A pure function of {@code (seed, cell, body index)}: no world, no {@code Random}, no registry, no + * tick. Ask it twice and it answers the same, which is the whole point — a telescope reports a world + * from across the system, and the landing has to match what the telescope said.

+ * + *

The order, and why it is this order

+ *
    + *
  1. Metallicity — one more seeded property of the star beside temperature and size. A + * metal-poor star formed a metal-poor disk, which is what makes the ore profile physical rather + * than tabulated.
  2. + *
  3. Orbital radius, drawn LOGARITHMICALLY: real systems are spaced roughly geometrically, and + * the range is anchored on the star's own {@linkplain #referenceDistance reference distance}, so + * zoning follows the star instead of a fixed table. A cool dwarf gets a compact system and a hot + * giant a sprawling one, for free.
  4. + *
  5. Bare temperature at that radius, with NO atmosphere. The snow line is this temperature + * crossing a threshold — never a separate parameter.
  6. + *
  7. Radius and mass, correlated with the zone: small rock inside, giants past the snow line. + * Gravity is DERIVED from them ({@code g = M/R²}); it is not drawn.
  8. + *
  9. Pressure, from the world's ability to hold an atmosphere against its own heat — heavy and + * cold retains, light and hot does not.
  10. + *
  11. Temperature again, now with that atmosphere. The greenhouse term needs a pressure, and + * the pressure needed a temperature; one pass each way resolves it without iterating, and the bare + * reading is kept for the zoning decisions that must not depend on the atmosphere.
  12. + *
  13. Type = a weighted draw among the presets that admit the resulting point. Zoning + * therefore EMERGES from the physics; no preset is placed anywhere by hand.
  14. + *
  15. Terrain from that type's weighted list, and finally the oxygen roll — biology on + * top of an already-suitable world, never a consequence of it.
  16. + *
+ * + *

Every constant below is a balance knob. None is a contract, and the class deliberately exposes the + * intermediate steps so a test can pin the RELATIONS (colder past the snow line, heavier holds more air) + * without pinning any of the numbers.

+ */ +public final class PlanetDerivation { + + // Salts, disjoint from ClusteredGalaxyGenerator's placement salts (0x1..0x15) and from each other. + private static final long SALT_METALLICITY = 0x21L; + private static final long SALT_ORBIT = 0x22L; + private static final long SALT_GIANT = 0x23L; + private static final long SALT_RADIUS = 0x24L; + private static final long SALT_DENSITY = 0x25L; + private static final long SALT_PRESSURE = 0x26L; + private static final long SALT_TYPE = 0x27L; + private static final long SALT_TERRAIN = 0x28L; + private static final long SALT_OXYGEN = 0x29L; + private static final long SALT_RINGS = 0x2AL; + + /** + * The temperature, in Kelvin, that defines a star's REFERENCE distance — Earth's equilibrium + * temperature with no atmosphere. Every orbital radius is drawn as a multiple of the distance at + * which this star produces it, so "the warm zone" means the same thing around every star. + */ + private static final double REFERENCE_TEMPERATURE_K = 255d; + + /** Innermost / outermost drawn orbit, as multiples of {@link #referenceDistance}. */ + private static final double INNER_ORBIT_FACTOR = 0.2d; + private static final double OUTER_ORBIT_FACTOR = 45d; + + /** + * Bare temperature below which volatiles freeze out — the SNOW LINE, expressed as the threshold it + * really is. Numerically the {@code FRIGID} band's floor, and deliberately the same number: a world + * the game calls frigid and a world past the snow line must be the same world. + */ + private static final int SNOW_LINE_K = 175; + + /** Probability that a body past the snow line accreted into a giant rather than staying a rock. */ + private static final double GIANT_CHANCE_OUTER = 0.34d; + /** The same, in the cool-but-not-frozen band just inside it. */ + private static final double GIANT_CHANCE_COOL = 0.06d; + /** Bare temperature below which the cool-band giant chance applies at all. */ + private static final int COOL_BAND_K = 260; + + /** Giant radius range, in Earth radii (Neptune ~3.9, Jupiter ~11). */ + private static final double GIANT_MIN_RADIUS = 3.0d; + private static final double GIANT_MAX_RADIUS = 11.0d; + /** Jupiter's mass in Earth masses, and the exponent that carries a smaller giant down from it. */ + private static final double JUPITER_MASSES = 318d; + private static final double GIANT_MASS_EXPONENT = 2.3d; + + /** Rocky radius draw: {@code MIN + u^BIAS · SPAN}, biased small so Earth-sized is the median. */ + private static final double ROCK_MIN_RADIUS = 0.2d; + private static final double ROCK_RADIUS_SPAN = 2.3d; + private static final double ROCK_RADIUS_BIAS = 1.7d; + /** A moon is drawn from the same law with a smaller span — moons are small by construction. */ + private static final double MOON_RADIUS_SPAN = 0.55d; + + /** Bulk density relative to Earth's, and the exponent that makes big rocky worlds denser. */ + private static final double MIN_DENSITY = 0.75d; + private static final double DENSITY_SPAN = 0.5d; + private static final double ROCK_MASS_EXPONENT = 3.7d; + + /** Gravity floor in g — the same floor the legacy random generator has always used. */ + private static final double MIN_GRAVITY_G = 0.05d; + + /** + * Atmospheric retention. {@code (M/R)} is escape velocity squared in Earth units; dividing by the + * bare temperature gives the Jeans-parameter shape — heavy and cold holds air, light and hot loses + * it. Normalised so Earth sits at 1, then raised to a steep power because the real transition from + * airless to crushing happens over a narrow range of that ratio. + */ + private static final double EARTH_RETENTION = 1d / (255d / 288d); + private static final double RETENTION_EXPONENT = 2.6d; + private static final double PRESSURE_SCATTER_MIN = 0.4d; + private static final double PRESSURE_SCATTER_SPAN = 2.6d; + + /** Chance that a world whose type PERMITS oxygen actually has it. Biology, so: rare. */ + private static final double OXYGEN_CHANCE = 0.18d; + + /** + * Ring chance for a giant, and for everything else. Rings are the debris of a moon that came apart + * inside its planet's Roche limit, and only a giant's limit reaches far enough beyond its own body + * for that to be a place a moon could ever have been — which is why all four Solar giants have them + * and none of the rocky planets does. + */ + private static final double RING_CHANCE_GIANT = 0.7d; + private static final double RING_CHANCE_ROCKY = 0.02d; + + /** + * Tidal-locking radius at one solar radius, in AU. Beyond a scale factor this is the real + * astronomical embarrassment about M-dwarf habitability: the locking radius shrinks far more slowly + * with the star than the warm zone does, so a cool dwarf's habitable orbits sit WELL inside it and + * its temperate worlds are locked, while a sunlike star's are not. + */ + private static final double TIDAL_LOCK_AU = 0.5d; + + /** Metallicity draw, relative to Sol. */ + private static final double MIN_METALLICITY = 0.35d; + private static final double METALLICITY_SPAN = 1.25d; + private static final double METALLICITY_BIAS = 1.3d; + + private PlanetDerivation() { + } + + // ─── The pieces, each answerable on its own ──────────────────────────────── + + /** + * The parent star's metal content relative to Sol. Keyed on the system's ANCHOR cell, not on the + * body, because it is a property of the star: every body of one system shares it. + */ + public static double metallicityOf(long seed, GalacticCoord anchor) { + double u = CellHash.norm(CellHash.ofCell(seed, anchor.cellCentre(), SALT_METALLICITY)); + return MIN_METALLICITY + Math.pow(u, METALLICITY_BIAS) * METALLICITY_SPAN; + } + + /** + * The orbital distance, in Advanced Rocketry units, at which this star warms a bare world to + * {@link #REFERENCE_TEMPERATURE_K}. One AU for Sol by construction; a tenth of that for a cool red + * dwarf; a dozen AU for a hot blue giant. + */ + public static int referenceDistance(StellarBody star) { + if (star == null) { + return AstronomicalBodyHelper.DISTANCE_UNITS_PER_AU; + } + // T falls as 1/sqrt(distance), so one probe at 1 AU fixes the whole curve. + int atOneAu = AstronomicalBodyHelper.getAverageTemperature(star, + AstronomicalBodyHelper.DISTANCE_UNITS_PER_AU, 0); + if (atOneAu <= 0) { + return AstronomicalBodyHelper.DISTANCE_UNITS_PER_AU; + } + double ratio = atOneAu / REFERENCE_TEMPERATURE_K; + double ref = AstronomicalBodyHelper.DISTANCE_UNITS_PER_AU * ratio * ratio; + return (int) clamp(ref, DimensionProperties.MIN_DISTANCE, 100_000d); + } + + /** + * The orbital distance of body {@code index} of {@code count}, drawn log-uniformly across the + * star's zone. + * + *

Each body owns a SLOT of the logarithmic range and is jittered inside it by less than half a + * slot, so the draw is irregular but the ordering is not: body {@code i} is always inside body + * {@code i+1}. That is what lets the placement map an orbit onto a cell radius monotonically, and it + * is why two bodies of one system cannot swap places when a tuning constant moves.

+ */ + public static int orbitalDistanceOf(long seed, GalacticCoord anchor, int index, int count, + StellarBody star) { + double lo = innerOrbit(star); + double hi = outerOrbit(star); + int slots = Math.max(1, count); + double jitter = 0.6d * (CellHash.norm(CellHash.ofBody(seed, anchor.cellCentre(), index, SALT_ORBIT)) + - 0.5d); + double f = (Math.min(index, slots - 1) + 0.5d + jitter) / slots; + double distance = lo * Math.pow(hi / lo, clamp(f, 0d, 1d)); + return (int) clamp(distance, DimensionProperties.MIN_DISTANCE, 1_000_000d); + } + + /** The innermost orbit this star's system may hold, in Advanced Rocketry distance units. */ + public static double innerOrbit(StellarBody star) { + return Math.max(DimensionProperties.MIN_DISTANCE, referenceDistance(star) * INNER_ORBIT_FACTOR); + } + + /** The outermost orbit this star's system may hold. Always comfortably above {@link #innerOrbit}. */ + public static double outerOrbit(StellarBody star) { + return Math.max(innerOrbit(star) * 1.5d, referenceDistance(star) * OUTER_ORBIT_FACTOR); + } + + /** + * Where {@code orbitalDistance} sits in this star's zone, as a fraction in {@code [0,1]} on a + * LOGARITHMIC scale — the inverse of the orbital draw. + * + *

This is what lets the galactic placement map an orbit onto a cell radius: the two layouts then + * agree by construction, so a body that is third from its star is also third out from the anchor + * cell, and neither can be re-tuned without the other following.

+ */ + public static double orbitFraction(int orbitalDistance, StellarBody star) { + double lo = innerOrbit(star); + double hi = outerOrbit(star); + if (!(hi > lo)) { + return 0d; + } + return clamp(Math.log(Math.max(lo, orbitalDistance) / lo) / Math.log(hi / lo), 0d, 1d); + } + + /** The bare (no-atmosphere) equilibrium temperature at a distance — the zoning reading. */ + public static int bareTemperature(StellarBody star, int orbitalDistance) { + return AstronomicalBodyHelper.getAverageTemperature(star, Math.max(1, orbitalDistance), 0); + } + + /** Whether a body this close to this star keeps one face to it. */ + public static boolean tidallyLockedAt(StellarBody star, int orbitalDistance) { + if (star == null) { + return false; + } + double lockDistance = AstronomicalBodyHelper.DISTANCE_UNITS_PER_AU * TIDAL_LOCK_AU + * Math.cbrt(Math.max(0.05d, star.getSize())); + return orbitalDistance <= lockDistance; + } + + /** Whether the body at this index accreted into a giant, given how cold its orbit is. */ + public static boolean isGiantAt(long seed, GalacticCoord anchor, int index, int bareTemperatureK) { + double chance = bareTemperatureK < SNOW_LINE_K ? GIANT_CHANCE_OUTER + : (bareTemperatureK < COOL_BAND_K ? GIANT_CHANCE_COOL : 0d); + if (chance <= 0d) { + return false; + } + return CellHash.norm(CellHash.ofBody(seed, anchor.cellCentre(), index, SALT_GIANT)) < chance; + } + + // ─── The whole derivation ────────────────────────────────────────────────── + + /** + * The full profile of a body, keyed on the cell it OCCUPIES rather than on its position in a list. + * + *

That choice is what makes a profile survive a pin. A cell name is durable for the life of the + * save; a body's index in the generator's output is not — it moves the moment a tuning constant + * changes the body count, and every planet in the system would then be a different world than the + * one a player scanned. Metallicity is the deliberate exception: it is a property of the STAR, so it + * is keyed on the anchor and shared by every body of the system.

+ * + * @param variant disambiguates bodies that legitimately SHARE a cell — a planet is 0 and its + * moons are 1, 2, … Without it a moon would draw its parent's exact physics, + * because it draws from its parent's cell by construction + * @param moon a satellite: never a giant, and drawn from a smaller size law + * @param orbitalDistance where the body sits, in Advanced Rocketry distance units. A moon takes its + * PARENT's, because what a moon's climate depends on is where the parent is + */ + public static BodyProfile derive(long seed, GalacticCoord anchor, GalacticCoord bodyCell, int variant, + StellarBody star, boolean moon, int orbitalDistance) { + GalacticCoord key = bodyCell.cellCentre(); + double metallicity = metallicityOf(seed, anchor); + int bareTemp = bareTemperature(star, orbitalDistance); + boolean giant = !moon && isGiantAt(seed, key, variant, bareTemp); + + double radius = radiusOf(seed, key, variant, giant, moon); + double mass = massOf(seed, key, variant, radius, giant); + int gravityPercent = gravityPercentOf(mass, radius); + int pressure = pressureOf(seed, key, variant, mass, radius, bareTemp, giant); + int temperature = AstronomicalBodyHelper.getAverageTemperature(star, + Math.max(1, orbitalDistance), pressure); + + PlanetTypePreset preset = PlanetTypes.drawType(pressure, temperature, gravityPercent, giant, + CellHash.ofBody(seed, key, variant, SALT_TYPE)); + TerrainOption terrain = PlanetTypes.drawTerrain(preset, + CellHash.ofBody(seed, key, variant, SALT_TERRAIN)); + + boolean oxygen = preset != null && preset.allowsOxygen() + && CellHash.norm(CellHash.ofBody(seed, key, variant, SALT_OXYGEN)) < OXYGEN_CHANCE; + boolean locked = (preset == null || preset.tidallyLockable()) && !giant + && tidallyLockedAt(star, orbitalDistance); + boolean rings = !moon + && CellHash.norm(CellHash.ofBody(seed, key, variant, SALT_RINGS)) + < (giant ? RING_CHANCE_GIANT : RING_CHANCE_ROCKY); + + SystemBodyKind kind = giant ? SystemBodyKind.GAS_GIANT + : (moon ? SystemBodyKind.MOON : SystemBodyKind.PLANET); + return new BodyProfile(kind, preset == null ? PlanetTypes.UNCLASSIFIED : preset.name(), preset, + orbitalDistance, mass, radius, gravityPercent, pressure, temperature, oxygen, locked, + rings, metallicity, terrain); + } + + // ─── The individual laws ─────────────────────────────────────────────────── + + private static double radiusOf(long seed, GalacticCoord cell, int index, boolean giant, boolean moon) { + double u = CellHash.norm(CellHash.ofBody(seed, cell, index, SALT_RADIUS)); + if (giant) { + return GIANT_MIN_RADIUS + u * (GIANT_MAX_RADIUS - GIANT_MIN_RADIUS); + } + double span = moon ? MOON_RADIUS_SPAN : ROCK_RADIUS_SPAN; + return ROCK_MIN_RADIUS + Math.pow(u, ROCK_RADIUS_BIAS) * span; + } + + private static double massOf(long seed, GalacticCoord cell, int index, double radius, boolean giant) { + if (giant) { + return JUPITER_MASSES * Math.pow(radius / GIANT_MAX_RADIUS, GIANT_MASS_EXPONENT); + } + double density = MIN_DENSITY + + CellHash.norm(CellHash.ofBody(seed, cell, index, SALT_DENSITY)) * DENSITY_SPAN; + // M = ρ·R^3.7 rather than ρ·R³: a bigger rocky world compresses its own interior, which is what + // stops a super-Earth's surface gravity from running away with the cube of its radius. + return density * Math.pow(radius, ROCK_MASS_EXPONENT); + } + + private static int gravityPercentOf(double mass, double radius) { + double g = mass / Math.max(1e-6d, radius * radius); + double clamped = clamp(g, MIN_GRAVITY_G, DimensionProperties.MAX_GRAVITY / 100d); + return (int) Math.round(clamped * 100d); + } + + private static int pressureOf(long seed, GalacticCoord cell, int index, double mass, double radius, + int bareTemperatureK, boolean giant) { + if (giant) { + return DimensionProperties.MAX_ATM_PRESSURE; + } + double retention = (mass / Math.max(1e-6d, radius)) + / Math.max(0.2d, bareTemperatureK / 288d); + double scatter = PRESSURE_SCATTER_MIN + + CellHash.norm(CellHash.ofBody(seed, cell, index, SALT_PRESSURE)) * PRESSURE_SCATTER_SPAN; + double raw = AstronomicalBodyHelper.ATM_PRESSURE_UNITS_PER_ATMOSPHERE + * Math.pow(retention / EARTH_RETENTION, RETENTION_EXPONENT) * scatter; + if (!(raw > 0d) || Double.isNaN(raw)) { + return DimensionProperties.MIN_ATM_PRESSURE; + } + return (int) clamp(Math.round(raw), DimensionProperties.MIN_ATM_PRESSURE, + DimensionProperties.MAX_ATM_PRESSURE); + } + + private static double clamp(double v, double lo, double hi) { + if (Double.isNaN(v)) { + return lo; + } + return v < lo ? lo : (v > hi ? hi : v); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java new file mode 100644 index 000000000..f737e6880 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java @@ -0,0 +1,260 @@ +package zmaster587.advancedRocketry.universe; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import net.minecraft.block.Block; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ResourceLocation; + +import zmaster587.advancedRocketry.api.Constants; +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.dimension.DimensionManager; +import zmaster587.advancedRocketry.dimension.DimensionProperties; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; +import zmaster587.advancedRocketry.util.XMLPlanetLoader; + +/** + * The seam where a scanned dot becomes a world: turning a procedural {@link SystemBody} into a real + * dimension a ship can put down on. + * + *

Without this the procedural galaxy is look-but-do-not-touch. Every body the generator places + * carries {@link Constants#INVALID_PLANET}, and {@code isDescendTarget()} is false for all of them, so a + * system full of planets has nowhere to land.

+ * + *

The four rules this class exists to keep

+ *
    + *
  1. A DESCENT realizes, and nothing else does. Scanning is cheap, remote and repeatable, and + * the tier schema answers a scan from the derivation on purpose — so minting on a scan would let + * one telescope sweep allocate dimensions by the dozen. Moons obey the same rule on their own + * account rather than being realized eagerly with a parent.
  2. + *
  3. Realization MATERIALIZES what was already derived; it never rolls fresh values. Mass, + * atmosphere, temperature and water are promised to a telescope from across the system, so a + * landing that disagreed with the scan would make the whole tier schema a lie. This is why + * {@code generateRandom} cannot be reused here: it walks a shared {@code Random}, allocates an id + * immediately, and seeds a biome roll from {@code System.nanoTime()} — none of which can answer + * the same question twice.
  4. + *
  5. After realization the SAVE is authoritative. The body is pinned, the dimension is + * registered and its properties are written down; a later seed, config, XML or modset change must + * not move or reshape a planet somebody has stood on.
  6. + *
  7. A realized planet is never un-realized. There is no eviction path here on purpose. A long + * game accumulates dimensions in proportion to the planets a player has actually LANDED on, which + * is bounded by play rather than by the size of the galaxy — and rule 1 is what keeps that bound + * tight.
  8. + *
+ * + *

Server main thread only.

+ */ +public final class PlanetRealizer { + + private static final Logger LOGGER = LogManager.getLogger("AdvancedRocketry|Universe"); + + private PlanetRealizer() { + } + + /** + * Realize the descend-target body standing in {@code bodyCell}, returning its dimension id — or + * {@link Constants#INVALID_PLANET} when that cell holds nothing anyone could land on. + * + *

Idempotent. A cell whose body already has a world answers with that world; a second + * descent into the same cell therefore reuses the dimension instead of minting another. This is the + * only entry point, so that "one body, one world" cannot be true in one caller and false in + * another.

+ */ + public static int realize(MinecraftServer server, GalacticCoord bodyCell) { + if (server == null || bodyCell == null) { + return Constants.INVALID_PLANET; + } + UniverseRegistry registry = UniverseRegistry.get(server); + if (registry == null) { + return Constants.INVALID_PLANET; + } + + // Pin FIRST. A touch is what freezes a procedural system into the save, and by the time this + // body has a dimension its surroundings must already be unable to drift away from under it. + registry.pinSystem(bodyCell); + + OptionalInt existing = registry.realizedDimAt(bodyCell); + if (existing.isPresent()) { + return existing.getAsInt(); + } + + Optional anchorOpt = registry.anchorForCell(bodyCell); + if (!anchorOpt.isPresent()) { + return Constants.INVALID_PLANET; + } + GalacticCoord anchor = anchorOpt.get(); + + List here = registry.bodiesAt(bodyCell); + SystemBody target = null; + int variant = 0; + int seen = 0; + for (SystemBody body : here) { + if (body.kind() == SystemBodyKind.STAR || body.kind() == SystemBodyKind.STATION_SLOT + || body.kind() == SystemBodyKind.ASTEROID_BELT) { + continue; + } + // The variant is a body's rank among the worlds SHARING this cell, and it must be counted + // exactly the way the generator assigned it — a planet is 0 and its moons follow — or a + // realized moon would materialize a different world than the one that was scanned. + if (target == null && body.kind().canDescend() + && body.dimId() == Constants.INVALID_PLANET) { + target = body; + variant = seen; + } + seen++; + } + if (target == null) { + return Constants.INVALID_PLANET; + } + + Optional starOpt = registry.starAt(bodyCell); + if (!starOpt.isPresent()) { + LOGGER.warn("[UNIVERSE] cannot realize the body at {}: its system has no star", bodyCell.cellKey()); + return Constants.INVALID_PLANET; + } + StellarBody star = starOpt.get(); + + // A procedural star keeps its SYNTHETIC NEGATIVE id — the pin already made that id a durable key + // in the save — but the catalogue has to learn about it, because a planet resolves its sun, + // its sky colour and its orbital period through the star list. + if (DimensionManager.getInstance().getStar(star.getId()) == null) { + DimensionManager.getInstance().addStar(star); + } else { + star = DimensionManager.getInstance().getStar(star.getId()); + } + + int dimId = DimensionManager.getInstance().getNextFreeDim(DimensionManager.dimOffset); + if (dimId == Constants.INVALID_PLANET) { + LOGGER.error("[UNIVERSE] no free dimension id left to realize the body at {}", bodyCell.cellKey()); + return Constants.INVALID_PLANET; + } + + BodyProfile profile = PlanetDerivation.derive(registry.worldSeed(), anchor, target.name(), variant, + star, target.kind() == SystemBodyKind.MOON, target.orbitalDistance()); + DimensionProperties props = materialize(dimId, profile, star, anchor, target); + + if (!DimensionManager.getInstance().registerDim(props, true)) { + LOGGER.error("[UNIVERSE] dimension {} was already registered while realizing {}", dimId, + bodyCell.cellKey()); + return Constants.INVALID_PLANET; + } + star.addPlanet(props); + if (!registry.realizeBody(bodyCell, dimId)) { + LOGGER.error("[UNIVERSE] realized dimension {} for {} but the body could not be rewritten - " + + "the world exists and nothing points at it", dimId, bodyCell.cellKey()); + return Constants.INVALID_PLANET; + } + LOGGER.info("[UNIVERSE] realized {} '{}' as dim {} at cell {} (type {}, {} K, {} atm-units, {}% g)", + profile.kind(), props.getName(), dimId, bodyCell.cellKey(), profile.typeName(), + profile.temperatureKelvin(), profile.pressure(), profile.gravityPercent()); + return dimId; + } + + /** + * Write a derived profile into a real {@link DimensionProperties}. Everything physical comes from + * the profile; everything cosmetic is derived from those same numbers, so nothing here consults a + * {@code Random}. + */ + private static DimensionProperties materialize(int dimId, BodyProfile profile, StellarBody star, + GalacticCoord anchor, SystemBody body) { + DimensionProperties props = new DimensionProperties(dimId); + props.setName(star.getName() + " " + dimId); + props.setStar(star); + + props.orbitalDist = Math.max(DimensionProperties.MIN_DISTANCE, profile.orbitalDistance()); + // The orbital angle is READ OFF the body's cell rather than drawn again, so the planet the sky + // shows and the planet the orbital elements describe are in the same place. + props.baseOrbitTheta = angleOf(anchor, body.name()); + props.orbitTheta = props.baseOrbitTheta; + + props.setAtmosphereDensityDirect(profile.pressure()); + props.averageTemperature = profile.temperatureKelvin(); + props.hasOxygen = profile.hasOxygen(); + props.setBulk(profile.massEarths(), profile.radiusEarths()); + props.setTidallyLocked(profile.tidallyLocked()); + props.setHasRings(profile.hasRings()); + props.setMetallicity(profile.metallicity()); + props.setGasGiant(profile.kind() == SystemBodyKind.GAS_GIANT); + props.rotationalPeriod = rotationalPeriodOf(profile, star); + + applyTerrain(props, profile.terrain()); + + PlanetTypePreset preset = profile.preset(); + if (preset != null) { + if (!preset.biomes().isEmpty()) { + XMLPlanetLoader.applyBiomeList(props, preset.biomes()); + } + if (preset.seaLevel() != PlanetTypePreset.SEA_LEVEL_UNSET) { + props.setSeaLevel(preset.seaLevel()); + } + if (!preset.oceanBlock().isEmpty()) { + Block block = Block.REGISTRY.getObject(new ResourceLocation(preset.oceanBlock())); + if (block != null) { + props.setOceanBlock(block.getDefaultState()); + } + } + if (preset.oreProperties() != null) { + props.oreProperties = preset.oreProperties(); + } + } + // No palette from the type: let the world derive one from its own climate, which is what an + // authored planet with no does. + if (props.getBiomes().isEmpty() && props.hasSurface()) { + props.addBiomes(props.getViableBiomes(true)); + } + props.initDefaultAttributes(); + return props; + } + + private static void applyTerrain(DimensionProperties props, TerrainOption terrain) { + if (terrain == null) { + return; + } + // Fixed HERE and never re-derived: from this point the save owns how this world generates, so a + // pack that later adds or removes a world generator cannot reshape ground somebody has walked on. + props.setTerrainSource(terrain.source()); + props.setTerrainWorldType(terrain.worldType()); + props.setTerrainTemplate(terrain.template()); + props.setTerrainGeneratorOptions(terrain.options()); + props.setGenType(terrain.genType()); + } + + /** + * How long this world's day is. A locked world's rotation IS its orbit — that is what locking means + * — and every other world keeps the legacy gravity-derived period so procedural planets have the + * same spread of day lengths the game has always had. + */ + private static int rotationalPeriodOf(BodyProfile profile, StellarBody star) { + if (profile.tidallyLocked()) { + double days = AstronomicalBodyHelper.getOrbitalPeriod(profile.orbitalDistance(), star.getSize()); + double ticks = days * AstronomicalBodyHelper.TICKS_PER_DAY; + if (!(ticks > 0d) || ticks > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int) ticks; + } + double gravity = Math.max(0.05d, profile.gravityPercent() / 100d); + double period = Math.pow(1d / gravity, 3) * DimensionProperties.DEFAULT_ROTATIONAL_PERIOD; + if (!(period > 0d) || period > Integer.MAX_VALUE) { + return DimensionProperties.DEFAULT_ROTATIONAL_PERIOD; + } + return Math.max(1, (int) period); + } + + /** The angle of a body's cell about its system's anchor, in radians. */ + private static double angleOf(GalacticCoord anchor, GalacticCoord bodyCell) { + long dx = bodyCell.sectorX() - anchor.sectorX(); + long dz = bodyCell.sectorZ() - anchor.sectorZ(); + if (dx == 0L && dz == 0L) { + return 0d; + } + double theta = Math.atan2((double) dz, (double) dx); + return theta < 0d ? theta + 2d * Math.PI : theta; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypePreset.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypePreset.java new file mode 100644 index 000000000..b2f01d19c --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypePreset.java @@ -0,0 +1,284 @@ +package zmaster587.advancedRocketry.universe; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import zmaster587.advancedRocketry.util.OreGenProperties; + +/** + * A planet type: the named region of physical parameter space a world can land in, together with + * everything that follows from being that kind of world. + * + *

There is no second "subtype" concept — a type IS this preset. One language: it declares the + * property ranges that admit a world, the weighted list of ways its terrain may be generated, its ore + * table and its native biome palette. Advanced Rocketry ships a stock set ({@link PlanetTypes}); a pack + * overrides them or adds its own, and a genuinely new class of world needs no code.

+ * + *

This object straddles the layer boundary on purpose, and the two halves are read by different + * layers. The UNIVERSE layer (a pure {@code (seed, cell)} derivation) reads only the numeric + * admission ranges, {@link #weight()}, {@link #gasGiant()}, {@link #allowsOxygen()} and the + * {@link #terrain()} weights — none of which touch a world, a registry or a block. The DIMENSION layer + * reads {@link #biomeIds()}, {@link #oreProperties()}, {@link #seaLevel()} and {@link #oceanBlock()} + * when it materializes a body into a real dimension. Nothing in the first list may be made to depend on + * the second, or the derivation stops being answerable from afar — which is the whole reason a scan can + * describe a world before anyone has been there.

+ * + *

Immutable; built through {@link #builder(String)}.

+ */ +public final class PlanetTypePreset { + + private final String name; + private final int weight; + private final int minPressure; + private final int maxPressure; + private final int minTemperature; + private final int maxTemperature; + private final int minGravity; + private final int maxGravity; + private final boolean gasGiant; + private final boolean allowsOxygen; + private final boolean tidallyLockable; + private final int seaLevel; + private final String oceanBlock; + private final List terrain; + private final String biomes; + private final OreGenProperties oreProperties; + + private PlanetTypePreset(Builder b) { + this.name = b.name; + this.weight = Math.max(1, b.weight); + this.minPressure = Math.min(b.minPressure, b.maxPressure); + this.maxPressure = Math.max(b.minPressure, b.maxPressure); + this.minTemperature = Math.min(b.minTemperature, b.maxTemperature); + this.maxTemperature = Math.max(b.minTemperature, b.maxTemperature); + this.minGravity = Math.min(b.minGravity, b.maxGravity); + this.maxGravity = Math.max(b.minGravity, b.maxGravity); + this.gasGiant = b.gasGiant; + this.allowsOxygen = b.allowsOxygen; + this.tidallyLockable = b.tidallyLockable; + this.seaLevel = b.seaLevel; + this.oceanBlock = b.oceanBlock == null ? "" : b.oceanBlock; + this.terrain = b.terrain.isEmpty() + ? Collections.singletonList(TerrainOption.ofNative(0, 1)) + : Collections.unmodifiableList(new ArrayList<>(b.terrain)); + this.biomes = b.biomes == null ? "" : b.biomes.trim(); + this.oreProperties = b.oreProperties; + } + + /** The type's name — what a scan reports and what a pack overrides by. */ + public String name() { + return name; + } + + /** Relative frequency among the presets that ALSO admit a given world. Never zero. */ + public int weight() { + return weight; + } + + /** Atmospheric pressure bound, in {@code DimensionProperties} atmosphere-density units (100 = 1 atm). */ + public int minPressure() { + return minPressure; + } + + public int maxPressure() { + return maxPressure; + } + + /** Surface temperature bound, in KELVIN — the unit {@code averageTemperature} is stored in. */ + public int minTemperature() { + return minTemperature; + } + + public int maxTemperature() { + return maxTemperature; + } + + /** Surface gravity bound, in PERCENT of Earth's ({@code MIN_GRAVITY}/{@code MAX_GRAVITY} units). */ + public int minGravity() { + return minGravity; + } + + public int maxGravity() { + return maxGravity; + } + + /** Whether this type describes a body with NO SURFACE — a giant, which is never landed on. */ + public boolean gasGiant() { + return gasGiant; + } + + /** + * Whether a world of this type may draw a breathable atmosphere at all. Oxygen is BIOLOGY, not + * physics: it is an independent rare roll over a world this flag permits, never a consequence of + * landing in the right pressure and temperature band. + */ + public boolean allowsOxygen() { + return allowsOxygen; + } + + /** + * Whether a world of this type can be tidally locked when it orbits close enough to be. A giant is + * excluded because nobody stands on one, so the permanent-day/permanent-night difficulty axis has + * nothing to act on. + */ + public boolean tidallyLockable() { + return tidallyLockable; + } + + /** Sea level for a realized world of this type, or {@link #SEA_LEVEL_UNSET} to keep the default. */ + public int seaLevel() { + return seaLevel; + } + + /** Registry name of the ocean fluid block, or empty for the default (water). */ + public String oceanBlock() { + return oceanBlock; + } + + /** Sentinel for {@link #seaLevel()}: this preset does not move the sea. */ + public static final int SEA_LEVEL_UNSET = -1; + + /** The weighted ways a world of this type may be generated. Never empty. */ + public List terrain() { + return terrain; + } + + /** + * This type's native biome palette, in the SAME authored form as a planet's {@code } + * element: a comma-separated list of {@code name;weight} or {@code id;weight} entries, empty for + * "let the world derive its own from its climate". + * + *

It is kept as the raw authored string rather than resolved ids because a biome's numeric id is + * assigned at registration time and differs between modsets — and because one format with one + * parser means a preset and a planet can never disagree about what an entry means.

+ */ + public String biomes() { + return biomes; + } + + /** This type's ore table, or {@code null} to fall back to the climate matrix. */ + public OreGenProperties oreProperties() { + return oreProperties; + } + + /** + * Whether a world at {@code pressure} / {@code temperatureKelvin} / {@code gravityPercent} lands + * inside this type's declared region, and agrees with it about having a surface. + * + *

Bounds are INCLUSIVE at both ends, so adjacent presets authored to touch ({@code max="175"} + * and {@code min="175"}) both admit the boundary rather than leaving a world with no type at all. + * Overlap is expected and resolved by a weighted draw — see {@link PlanetTypes}.

+ */ + public boolean admits(int pressure, int temperatureKelvin, int gravityPercent, boolean isGasGiant) { + return isGasGiant == gasGiant + && pressure >= minPressure && pressure <= maxPressure + && temperatureKelvin >= minTemperature && temperatureKelvin <= maxTemperature + && gravityPercent >= minGravity && gravityPercent <= maxGravity; + } + + public static Builder builder(String name) { + return new Builder(name); + } + + @Override + public String toString() { + return "PlanetTypePreset[" + name + " w=" + weight + " p=" + minPressure + ".." + maxPressure + + " T=" + minTemperature + ".." + maxTemperature + " g=" + minGravity + ".." + maxGravity + + (gasGiant ? " giant" : "") + ']'; + } + + /** Mutable builder — the authored form, used by both the stock table and the XML reader. */ + public static final class Builder { + private final String name; + private int weight = 10; + private int minPressure; + private int maxPressure = 1600; + private int minTemperature; + private int maxTemperature = 5000; + private int minGravity; + private int maxGravity = 400; + private boolean gasGiant; + private boolean allowsOxygen; + private boolean tidallyLockable = true; + private int seaLevel = SEA_LEVEL_UNSET; + private String oceanBlock = ""; + private final List terrain = new ArrayList<>(); + private String biomes = ""; + private OreGenProperties oreProperties; + + private Builder(String name) { + this.name = name == null ? "" : name.trim(); + } + + public Builder weight(int w) { + this.weight = w; + return this; + } + + public Builder pressure(int min, int max) { + this.minPressure = min; + this.maxPressure = max; + return this; + } + + public Builder temperature(int min, int max) { + this.minTemperature = min; + this.maxTemperature = max; + return this; + } + + public Builder gravity(int min, int max) { + this.minGravity = min; + this.maxGravity = max; + return this; + } + + public Builder gasGiant(boolean g) { + this.gasGiant = g; + return this; + } + + public Builder allowsOxygen(boolean o) { + this.allowsOxygen = o; + return this; + } + + public Builder tidallyLockable(boolean t) { + this.tidallyLockable = t; + return this; + } + + public Builder seaLevel(int level) { + this.seaLevel = level; + return this; + } + + public Builder oceanBlock(String registryName) { + this.oceanBlock = registryName; + return this; + } + + public Builder terrain(TerrainOption option) { + if (option != null) { + this.terrain.add(option); + } + return this; + } + + /** The raw {@code } palette string — see {@link PlanetTypePreset#biomes()}. */ + public Builder biomes(String authoredList) { + this.biomes = authoredList; + return this; + } + + public Builder ores(OreGenProperties ores) { + this.oreProperties = ores; + return this; + } + + public PlanetTypePreset build() { + return new PlanetTypePreset(this); + } + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java new file mode 100644 index 000000000..09c03dfe8 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java @@ -0,0 +1,310 @@ +package zmaster587.advancedRocketry.universe; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * The catalogue of {@link PlanetTypePreset planet types} and the two draws that use it: which type a + * derived world IS, and which of that type's terrain generators it gets. + * + *

The stock set ships in CODE and is overridden wholesale by the {@code } elements of + * {@code planetDefs.xml}. XML is authoritative when present; the code answers when it is not, so a + * trimmed or broken config degrades to stock worlds instead of producing worlds with no type at all. + * Same shape as {@code }.

+ * + *

Two rules that are not obvious from the signatures

+ *
    + *
  • Overlap is resolved by a WEIGHTED DRAW among every admitting preset, never by first + * match. First match would make the XML's document ORDER load-bearing — a silent dependency an + * author cannot see — and an explicit priority attribute would be a second ordering language for + * something weights already express. The consequence, which is tuning and not design: a preset + * with wide ranges soaks probability from narrow ones, so the stock ranges are authored tight.
  • + *
  • The availability filter runs BEFORE the terrain draw, never after. An entry naming a + * {@code WorldType} this modset does not have is dropped and the remaining weights renormalize by + * themselves. Filtering after the draw would silently convert that entry's whole share into the + * fallback — so removing one mod would not merely remove its worlds, it would make some other + * kind of world commoner in exact proportion.
  • + *
+ * + *

Static state, server-side authored config — the same lifetime and the same reset points as the + * star catalogue it is loaded beside.

+ */ +public final class PlanetTypes { + + // A self-contained logger rather than AdvancedRocketry.logger: loading the mod class triggers Forge + // bootstrap, which would break pure unit tests of the derivation this class feeds. + private static final Logger LOGGER = LogManager.getLogger("AdvancedRocketry|Universe"); + + /** + * The name reported for a world no preset admits. It is never drawn — it exists so that a hole in + * the authored coverage produces a world that is still landable and still describable, rather than + * a null type nothing downstream can render. Seeing it in a log means the preset table has a gap. + */ + public static final String UNCLASSIFIED = "unclassified"; + + /** + * Whether a foreign {@code WorldType} of this name exists in the running modset. A seam, so the + * filter is unit-testable without a Minecraft registry; production resolves it against + * {@code WorldType.byName}. + */ + private static volatile Predicate worldTypeAvailable = PlanetTypes::worldTypeIsRegistered; + + private static volatile List presets = stockPresets(); + + private PlanetTypes() { + } + + // ─── The catalogue ───────────────────────────────────────────────────────── + + /** Every preset currently in force, in authored order. Never empty. */ + public static List presets() { + return presets; + } + + /** Install an authored table (the {@code } elements). An empty list restores stock. */ + public static void setPresets(List authored) { + if (authored == null || authored.isEmpty()) { + presets = stockPresets(); + return; + } + presets = Collections.unmodifiableList(new ArrayList<>(authored)); + } + + /** Restore the code-shipped table — the world-unload / config-reset path. */ + public static void resetToStock() { + presets = stockPresets(); + } + + /** The preset of that name, or {@code null}. */ + public static PlanetTypePreset byName(String name) { + if (name == null) { + return null; + } + for (PlanetTypePreset p : presets) { + if (p.name().equalsIgnoreCase(name)) { + return p; + } + } + return null; + } + + /** Override the {@code WorldType}-availability probe (tests, or an addon with its own registry). */ + public static void setWorldTypeAvailability(Predicate probe) { + worldTypeAvailable = probe == null ? PlanetTypes::worldTypeIsRegistered : probe; + } + + // ─── The draws ───────────────────────────────────────────────────────────── + + /** Every preset whose declared region admits this world. May be empty (an authoring gap). */ + public static List candidates(int pressure, int temperatureKelvin, + int gravityPercent, boolean gasGiant) { + List out = new ArrayList<>(); + for (PlanetTypePreset p : presets) { + if (p.admits(pressure, temperatureKelvin, gravityPercent, gasGiant)) { + out.add(p); + } + } + return out; + } + + /** + * The type of a world at these parameters, drawn by weight among everything that admits it. + * {@code hash} is the derivation's own draw — the same {@code (seed, cell)} always lands on the + * same type. + * + *

When nothing admits the world, the WIDEST admitting-by-temperature stock shape is not + * substituted and no preset is invented: the answer is {@code null}, and the caller reports the + * world as {@link #UNCLASSIFIED}. A silent substitution would hide the authoring gap forever.

+ */ + public static PlanetTypePreset drawType(int pressure, int temperatureKelvin, int gravityPercent, + boolean gasGiant, long hash) { + List admitting = candidates(pressure, temperatureKelvin, gravityPercent, gasGiant); + if (admitting.isEmpty()) { + if (SystemContent.reportOnce("noPlanetType:" + gasGiant + ':' + pressure / 50 + ':' + + temperatureKelvin / 25 + ':' + gravityPercent / 25)) { + LOGGER.warn("no planet type admits a world at pressure {}, {} K, gravity {}% (gasGiant={})" + + " - it will be reported as '{}'. Widen a range to cover it.", + pressure, temperatureKelvin, gravityPercent, gasGiant, UNCLASSIFIED); + } + return null; + } + long total = 0L; + for (PlanetTypePreset p : admitting) { + total += p.weight(); + } + long r = Math.floorMod(hash, Math.max(1L, total)); + for (PlanetTypePreset p : admitting) { + if (r < p.weight()) { + return p; + } + r -= p.weight(); + } + return admitting.get(admitting.size() - 1); + } + + /** + * The terrain generator a world of type {@code preset} gets, drawn by weight over the entries this + * modset can actually run. Never {@code null}: a preset whose every entry names a missing mod falls + * back to Advanced Rocketry's own generator, which is the one thing always present. + */ + public static TerrainOption drawTerrain(PlanetTypePreset preset, long hash) { + if (preset == null) { + return TerrainOption.ofNative(0, 1); + } + // D6: drop the unavailable entries FIRST, then draw over what is left. + List available = new ArrayList<>(); + for (TerrainOption option : preset.terrain()) { + if (!option.needsForeignWorldType() || worldTypeAvailable.test(option.worldType())) { + available.add(option); + } + } + if (available.isEmpty()) { + if (SystemContent.reportOnce("noTerrain:" + preset.name())) { + LOGGER.warn("planet type '{}' has no runnable terrain source in this modset (every " + + " entry names a WorldType that is not registered) - falling back to the " + + "native generator.", preset.name()); + } + return TerrainOption.ofNative(0, 1); + } + long total = 0L; + for (TerrainOption option : available) { + total += option.weight(); + } + long r = Math.floorMod(hash, Math.max(1L, total)); + for (TerrainOption option : available) { + if (r < option.weight()) { + return option; + } + r -= option.weight(); + } + return available.get(available.size() - 1); + } + + // ─── The stock table ─────────────────────────────────────────────────────── + + /** + * The code-shipped presets. Ranges are authored TIGHT and made to TOUCH rather than overlap + * broadly: a wide preset soaks probability from every narrow one it contains, so an "everything + * else" catch-all would quietly become the commonest world in the galaxy. + * + *

Astronomy on the left of each comment, the Advanced Rocketry lever it is expressed through on + * the right. Every number here is a balance knob and none of them is a contract.

+ */ + public static List stockPresets() { + List l = new ArrayList<>(); + + // The commonest body class of all — every airless moon, Mercury. Defined by having no air at + // all, which is why its pressure band is the tight one and its temperature band is not: an + // airless rock is as plausible baking beside its star as frozen far from it. + l.add(PlanetTypePreset.builder("barren").weight(30) + .pressure(0, 25).temperature(0, 1500).gravity(1, 90) + .biomes("advancedrocketry:moon;30,advancedrocketry:moondark;20") + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // Everything past the snow line, thin-aired or thick: Europa and Titan are the same class of + // world, and which of the two you get is how much nitrogen the gravity managed to keep. + l.add(PlanetTypePreset.builder("ice").weight(22) + .pressure(0, 1600).temperature(0, 200).gravity(1, 400) + .biomes("advancedrocketry:moondark;10,minecraft:ice_flats;30,minecraft:ice_mountains;20") + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // Tight inner orbits, common around M dwarfs. A molten surface under whatever the rock itself + // boiled off, which can be a great deal — hence no pressure ceiling. + l.add(PlanetTypePreset.builder("lava").weight(12) + .pressure(0, 1600).temperature(700, 6000).gravity(5, 400) + .biomes("advancedrocketry:volcanic;30,advancedrocketry:volcanicbarren;20," + + "advancedrocketry:hotdryrock;10") + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // Venus-like, and likely common in the hot zone: a thick atmosphere doing the warming, which is + // why the band is keyed on the PRESSURE floor rather than on where the world orbits. + l.add(PlanetTypePreset.builder("greenhouse").weight(14) + .pressure(150, 1600).temperature(275, 1000).gravity(20, 400) + .biomes("advancedrocketry:hotdryrock;30,advancedrocketry:volcanicbarren;10") + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // The commonest planet class in the galaxy, and absent from the Solar System entirely. Defined + // by MASS, not by climate: a super-Earth is one whether it is frozen or baked. + l.add(PlanetTypePreset.builder("superearth").weight(16) + .pressure(0, 1600).temperature(0, 900).gravity(160, 400) + .biomes("advancedrocketry:stormland;30,advancedrocketry:hotdryrock;10") + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // A common end state of water loss: warm, dry, and holding just enough air to blow it around. + l.add(PlanetTypePreset.builder("desert").weight(16) + .pressure(0, 200).temperature(200, 700).gravity(10, 200) + .biomes("advancedrocketry:hotdryrock;30,minecraft:desert;20,minecraft:mesa;10") + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // Hypothesised but plausible: no exposed continent worth the name, and a deep global sea. + l.add(PlanetTypePreset.builder("ocean").weight(7).allowsOxygen(true) + .pressure(60, 400).temperature(255, 380).gravity(50, 190) + .seaLevel(96) + .biomes("advancedrocketry:oceanspires;30,minecraft:deep_ocean;30,minecraft:ocean;20") + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // Life without oxygen — the crystal / stormland / alien-forest biomes, all written and nearly + // unused today. Deliberately narrow: a find, not a background. + l.add(PlanetTypePreset.builder("exotic").weight(5) + .pressure(40, 1600).temperature(200, 430).gravity(10, 220) + .biomes("advancedrocketry:crystalchasms;30,advancedrocketry:stormland;20," + + "advancedrocketry:alien_forest;10") + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // Very rare, and rare on purpose: the conjunction is physics, the oxygen on top is biology. + l.add(PlanetTypePreset.builder("earthlike").weight(3).allowsOxygen(true) + .pressure(50, 220).temperature(255, 325).gravity(60, 145) + .biomes("minecraft:plains;30,minecraft:forest;25,minecraft:extreme_hills;15," + + "minecraft:ocean;15,advancedrocketry:marsh;10") + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // ~10-20% of stars. A real destination — fuel skimming and moons — but never a landing. + // Its bands are deliberately the widest in the table: a giant is a giant, and nothing else in + // this list will ever admit one, so a gap here would leave a whole body class untyped. + l.add(PlanetTypePreset.builder("gasgiant").weight(14).gasGiant(true).tidallyLockable(false) + .pressure(0, 1600).temperature(0, 1500).gravity(1, 400) + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + // Neptune and Uranus: the same, further out and colder. + l.add(PlanetTypePreset.builder("icegiant").weight(9).gasGiant(true).tidallyLockable(false) + .pressure(0, 1600).temperature(0, 250).gravity(1, 300) + .terrain(TerrainOption.ofNative(0, 1)) + .build()); + + return Collections.unmodifiableList(l); + } + + /** + * Production availability probe. Kept out of the field initialiser so that a unit test which never + * declares a foreign generator never loads a Minecraft registry class. + */ + private static boolean worldTypeIsRegistered(String name) { + if (name == null || name.trim().isEmpty()) { + return false; + } + try { + // The SAME resolver TerrainResolution uses when it actually installs the generator — a + // filter that admitted a name the installer then rejects would be worse than no filter. + return net.minecraft.world.WorldType.parseWorldType(name.trim()) != null; + } catch (Throwable t) { + // No registry in this context (a headless derivation) — treat the generator as absent + // rather than pretending it is there and handing a realized world a name nothing answers. + return false; + } + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java index 93c97e831..08586963d 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java @@ -39,12 +39,16 @@ public final class SystemBody { /** No content may sit outside its own cell — a cell is a whole neighbourhood — so an offset is bounded. */ private static final long MAX_IN_CELL = GalacticCoord.HALF_CELL - 1L; + /** Sentinel for {@link #orbitalDistance()}: this body has no orbit of its own (a star, a POI). */ + public static final int ORBIT_UNKNOWN = 0; + private final GalacticCoord name; private final CellFrame frame; private final BodyEphemeris offsetLaw; private final SystemBodyKind kind; private final int dimId; private final int starId; + private final int orbitalDistance; /** * A body at rest in a STATIC frame — the reading for a POI, a fixture, or anything derived @@ -52,13 +56,24 @@ public final class SystemBody { * the (constant) in-cell offset. */ public SystemBody(GalacticCoord address, SystemBodyKind kind, int dimId, int starId) { + this(address, kind, dimId, starId, ORBIT_UNKNOWN); + } + + /** The same, carrying the body's orbital radius — see {@link #orbitalDistance()}. */ + public SystemBody(GalacticCoord address, SystemBodyKind kind, int dimId, int starId, + int orbitalDistance) { this(requireAddress(address).cellCentre(), CellFrame.staticAt(address), BodyEphemeris.fixed(address.localX(), address.localY(), address.localZ()), - kind, dimId, starId); + kind, dimId, starId, orbitalDistance); } public SystemBody(GalacticCoord name, CellFrame frame, BodyEphemeris offsetLaw, SystemBodyKind kind, int dimId, int starId) { + this(name, frame, offsetLaw, kind, dimId, starId, ORBIT_UNKNOWN); + } + + public SystemBody(GalacticCoord name, CellFrame frame, BodyEphemeris offsetLaw, + SystemBodyKind kind, int dimId, int starId, int orbitalDistance) { if (name == null) { throw new NullPointerException("name"); } @@ -71,6 +86,7 @@ public SystemBody(GalacticCoord name, CellFrame frame, BodyEphemeris offsetLaw, this.kind = kind; this.dimId = dimId; this.starId = starId; + this.orbitalDistance = orbitalDistance; } private static GalacticCoord requireAddress(GalacticCoord address) { @@ -138,6 +154,31 @@ public int starId() { return starId; } + /** + * How far this body orbits its primary, in Advanced Rocketry distance units (100 = 1 AU), or + * {@link #ORBIT_UNKNOWN} for a body with no orbit of its own. + * + *

It travels WITH the body rather than being recomputed from the body's cell, because a cell is + * coarse — a whole neighbourhood — while the orbit is what every physical property of the world is + * derived from. Recovering it from the address would make a planet's temperature a function of the + * placement arithmetic, so a tuning change to the layout would silently re-climate every world in + * the galaxy.

+ */ + public int orbitalDistance() { + return orbitalDistance; + } + + /** + * This body with a realized dimension attached. Used exactly once per body, when a descent turns it + * from a scanned dot into a world; everything else about it — its name, its frame, its orbit — is + * carried over untouched, because realization materializes what was already derived and changes + * nothing about where the body is. + */ + public SystemBody withDimId(int newDimId) { + return newDimId == dimId ? this + : new SystemBody(name, frame, offsetLaw, kind, newDimId, starId, orbitalDistance); + } + /** {@code true} iff this body can be descended into as a walkable dimension. */ public boolean isDescendTarget() { return kind.canDescend() && dimId != Constants.INVALID_PLANET; @@ -161,7 +202,7 @@ public boolean definesFrame() { public SystemBody withFrame(CellFrame newFrame) { return newFrame == null || newFrame.equals(frame) ? this - : new SystemBody(name, newFrame, offsetLaw, kind, dimId, starId); + : new SystemBody(name, newFrame, offsetLaw, kind, dimId, starId, orbitalDistance); } public void writeToNBT(NBTTagCompound nbt) { @@ -171,6 +212,9 @@ public void writeToNBT(NBTTagCompound nbt) { nbt.setString("kind", kind.name()); nbt.setInteger("dimId", dimId); nbt.setInteger("starId", starId); + if (orbitalDistance != ORBIT_UNKNOWN) { + nbt.setInteger("orbitalDist", orbitalDistance); + } } public static SystemBody readFromNBT(NBTTagCompound nbt) { @@ -184,7 +228,8 @@ public static SystemBody readFromNBT(NBTTagCompound nbt) { return new SystemBody(name, CellFrame.readFromNBT(nbt, name), BodyEphemeris.readFromNBT(nbt), kind, nbt.hasKey("dimId") ? nbt.getInteger("dimId") : Constants.INVALID_PLANET, - nbt.getInteger("starId")); + nbt.getInteger("starId"), + nbt.getInteger("orbitalDist")); } @Override @@ -197,6 +242,7 @@ public boolean equals(Object o) { } SystemBody other = (SystemBody) o; return dimId == other.dimId && starId == other.starId && kind == other.kind + && orbitalDistance == other.orbitalDistance && name.equals(other.name) && offsetLaw.equals(other.offsetLaw) && frame.equals(other.frame); } @@ -207,6 +253,7 @@ public int hashCode() { result = 31 * result + kind.hashCode(); result = 31 * result + dimId; result = 31 * result + starId; + result = 31 * result + orbitalDistance; result = 31 * result + offsetLaw.hashCode(); return result; } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java index 485d93337..074e7a822 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java @@ -150,8 +150,12 @@ public static List bodiesOf(StellarBody star, GalacticCoord systemCo BodyEphemeris planetLaw = orbitLawOf(planet, star); GalacticCoord planetName = nameOf(planet, planetLaw, anchor, minSpacingCells, starId, names); CellFrame planetFrame = CellFrame.of(anchorAbs, planetLaw); + // The orbit travels on the body for authored systems too, so the field means the same thing + // for the whole catalogue: how far this body is from its star. A body that knew its orbit + // only when it was procedural would be a field that lies for half the galaxy. bodies.add(new SystemBody(planetName, planetFrame, BodyEphemeris.STATIC, - kindOf(planet, SystemBodyKind.PLANET), planet.getId(), starId)); + kindOf(planet, SystemBodyKind.PLANET), planet.getId(), starId, + planet.getOrbitalDist())); for (int moonId : planet.getChildPlanets()) { DimensionProperties moon = DimensionManager.getInstance().getDimensionProperties(moonId); @@ -160,8 +164,12 @@ public static List bodiesOf(StellarBody star, GalacticCoord systemCo } // A moon shares its parent's NAME and its parent's FRAME, and keeps its own live offset // inside it: a planet-and-its-moons is one destination that moves as one. + // A moon carries its PARENT's distance from the star — what warms a moon is where its + // planet is; how far it sits from the planet is in its ephemeris, which is what + // positions it. Same convention as the procedural side. bodies.add(new SystemBody(planetName, planetFrame, moonLawOf(moon, planet), - kindOf(moon, SystemBodyKind.MOON), moon.getId(), starId)); + kindOf(moon, SystemBodyKind.MOON), moon.getId(), starId, + planet.getOrbitalDist())); } } auditOneRealBodyPerCell(bodies, starId); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/TerrainOption.java b/src/main/java/zmaster587/advancedRocketry/universe/TerrainOption.java new file mode 100644 index 000000000..8c79ec9a1 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/TerrainOption.java @@ -0,0 +1,129 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.dimension.TerrainSource; + +/** + * One weighted entry of a {@link PlanetTypePreset}'s terrain list: a way this kind of world may be + * generated, plus how often it is chosen relative to the type's other entries. + * + *

A third-party world generator is a FIRST-CLASS terrain source here, not a fallback. A foreign + * generator brings authored terrain logic, so its variety does not degrade with repetition the way one + * procedural pen does — which is why a preset declares a LIST rather than a single generator.

+ * + *

The three shapes mirror {@link TerrainSource}: {@link TerrainSource#NATIVE} carries a + * {@link #genType()} (Advanced Rocketry's own sub-flavour selector), {@link TerrainSource#MOD_WORLDTYPE} + * a {@link #worldType()} name resolved against the live {@code WorldType} registry, and + * {@link TerrainSource#TEMPLATE} a {@link #template()} folder name. {@link #options()} is the + * per-dimension generator-settings string handed to whichever generator is drawn; empty means + * "your defaults".

+ * + *

Immutable and free of world state: a draw over these is part of a pure derivation.

+ */ +public final class TerrainOption { + + private final TerrainSource source; + private final String worldType; + private final String template; + private final int genType; + private final String options; + private final int weight; + + public TerrainOption(TerrainSource source, String worldType, String template, int genType, + String options, int weight) { + this.source = source == null ? TerrainSource.NATIVE : source; + this.worldType = worldType == null ? "" : worldType.trim(); + this.template = template == null ? "" : template.trim(); + this.genType = Math.max(0, genType); + this.options = options == null ? "" : options; + // A zero or negative weight would silently drop the entry from every draw while still LOOKING + // authored; floor it at 1 so "present in the XML" and "reachable" mean the same thing. + this.weight = Math.max(1, weight); + } + + /** Advanced Rocketry's own generator, sub-flavour {@code genType}. */ + public static TerrainOption ofNative(int genType, int weight) { + return new TerrainOption(TerrainSource.NATIVE, "", "", genType, "", weight); + } + + /** A foreign {@code WorldType}, resolved by name, with an optional generator-settings string. */ + public static TerrainOption ofWorldType(String worldTypeName, String options, int weight) { + return new TerrainOption(TerrainSource.MOD_WORLDTYPE, worldTypeName, "", 0, options, weight); + } + + /** Pre-generated region files loaded verbatim from {@code config/advRocketry/templates//}. */ + public static TerrainOption ofTemplate(String templateName, int weight) { + return new TerrainOption(TerrainSource.TEMPLATE, "", templateName, 0, "", weight); + } + + public TerrainSource source() { + return source; + } + + public String worldType() { + return worldType; + } + + public String template() { + return template; + } + + public int genType() { + return genType; + } + + public String options() { + return options; + } + + public int weight() { + return weight; + } + + /** + * Whether this entry names a generator supplied by another mod — the only kind that can be MISSING + * from a given modset, and therefore the only kind the availability filter has anything to say + * about. + */ + public boolean needsForeignWorldType() { + return source == TerrainSource.MOD_WORLDTYPE && !worldType.isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TerrainOption)) { + return false; + } + TerrainOption other = (TerrainOption) o; + return source == other.source && genType == other.genType && weight == other.weight + && worldType.equals(other.worldType) && template.equals(other.template) + && options.equals(other.options); + } + + @Override + public int hashCode() { + int result = source.hashCode(); + result = 31 * result + worldType.hashCode(); + result = 31 * result + template.hashCode(); + result = 31 * result + genType; + result = 31 * result + options.hashCode(); + return 31 * result + weight; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("TerrainOption[").append(source); + if (!worldType.isEmpty()) { + sb.append(' ').append(worldType); + } + if (!template.isEmpty()) { + sb.append(' ').append(template); + } + if (source == TerrainSource.NATIVE) { + sb.append(" genType=").append(genType); + } + return sb.append(" w=").append(weight).append(']').toString(); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java index bca782771..6d7c256ec 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java @@ -628,6 +628,84 @@ public boolean pinSystem(GalacticCoord coord) { return true; } + /** + * The star of the system whose neighbourhood contains {@code coord} — pinned snapshot, catalogue + * entry, or the generator's fabrication, in that order. + * + *

The pin comes FIRST and that ordering is the point: a touched procedural system's star is + * frozen in the save, so a later seed or config edit cannot warm it up under the planets that were + * derived from it. Realization needs this to materialize a body's physics, and the star it uses must + * be the one the scan already described.

+ */ + public Optional starAt(GalacticCoord coord) { + Optional anchorOpt = anchorForCell(coord); + if (!anchorOpt.isPresent()) { + return Optional.empty(); + } + GalacticCoord anchor = anchorOpt.get(); + PinnedSystem pinned = pinnedSystems.get(anchor.cellKey()); + if (pinned != null) { + return Optional.of(pinned.toStar()); + } + Integer id = byCell.get(anchor.cellKey()); + if (id != null) { + return Optional.ofNullable(starLookup.apply(id)); + } + Optional sys = generator.systemAt(worldSeed, anchor); + return sys.isPresent() ? Optional.of(sys.get().star()) : Optional.empty(); + } + + /** + * Attach a realized dimension to the pinned body standing at {@code bodyCell}, and record that + * cell as the dimension's durable NAME. Returns whether a body was rewritten. + * + *

Only a PINNED system can be rewritten, and that is not a limitation but the mechanism: a body + * is pinned the moment anything touches it, so by the time a descent asks for a dimension the + * snapshot it is being written into already exists. Rewriting a derived body would be writing into + * a list that is regenerated on the next query.

+ * + *

Idempotent by construction — a body that already carries this dimension is left exactly as it + * is, so a second descent into the same cell reuses the world rather than minting another.

+ */ + public boolean realizeBody(GalacticCoord bodyCell, int dimId) { + Optional anchorOpt = anchorForCell(bodyCell); + if (!anchorOpt.isPresent()) { + return false; + } + PinnedSystem pinned = pinnedSystems.get(anchorOpt.get().cellKey()); + if (pinned == null) { + return false; + } + GalacticCoord cell = bodyCell.cellCentre(); + for (int i = 0; i < pinned.bodies.size(); i++) { + SystemBody body = pinned.bodies.get(i); + if (!body.kind().canDescend() || !body.name().sameCell(cell)) { + continue; + } + if (body.dimId() == dimId) { + return true; + } + if (body.dimId() != Constants.INVALID_PLANET) { + continue; // another body of this cell (a moon) already holds a world of its own + } + pinned.bodies.set(i, body.withDimId(dimId)); + namesByDim.put(dimId, new RecordedName(cell, pinned.starId)); + markDirty(); + return true; + } + return false; + } + + /** The realized dimension of the descend-target body at {@code bodyCell}, if it has one. */ + public OptionalInt realizedDimAt(GalacticCoord bodyCell) { + for (SystemBody body : bodiesAt(bodyCell)) { + if (body.kind().canDescend() && body.dimId() != Constants.INVALID_PLANET) { + return OptionalInt.of(body.dimId()); + } + } + return OptionalInt.empty(); + } + /** The POIs at a system's cell (a copy), excluding the derived star/planet/moon bodies. */ public List poisAt(GalacticCoord systemCoord) { List list = poiOverrides.get(systemCoord.cellCentre().cellKey()); diff --git a/src/main/java/zmaster587/advancedRocketry/util/OreGenProperties.java b/src/main/java/zmaster587/advancedRocketry/util/OreGenProperties.java index 459ff8675..97238f50f 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/OreGenProperties.java +++ b/src/main/java/zmaster587/advancedRocketry/util/OreGenProperties.java @@ -55,6 +55,70 @@ public List getOreEntries() { return oreEntries; } + /** + * A COPY of this table with the metallic entries scaled by {@code factor} — the parent star's metal + * content applied to the palette its planet's climate earned. + * + *

A copy and never a mutation: a table from {@link #getOresForPressure} is shared by every world + * in that climate cell, so scaling it in place would give one planet's star the ore of all of them. + * Non-metallic entries (coal, redstone, lapis, diamond, emerald, quartz — and anything the ore + * dictionary does not call an ore at all) pass through untouched: a metal-poor disk yields the same + * SORTS of rock with less metal in them, which is what the physics actually says.

+ * + *

Both the clump size and the per-chunk chance are scaled, so the effect is on how much metal a + * world holds rather than on where it hides; each stays at least 1 so a scaling can thin a deposit + * but never delete it.

+ */ + public OreGenProperties withMetalsScaled(double factor) { + OreGenProperties copy = new OreGenProperties(); + for (OreEntry e : oreEntries) { + boolean metal = isMetallic(e.getBlockState()); + double f = metal ? Math.max(0.05d, factor) : 1d; + copy.addEntry(e.getBlockState(), e.getMinHeight(), e.getMaxHeight(), + scale(e.getClumpSize(), f), scale(e.getChancePerChunk(), f)); + } + return copy; + } + + private static int scale(int value, double factor) { + return Math.max(1, (int) Math.round(value * factor)); + } + + /** Ore-dictionary names that begin with {@code ore} but are not metals. */ + private static final java.util.Set NON_METAL_ORES = new java.util.HashSet<>( + java.util.Arrays.asList("orecoal", "oreredstone", "orelapis", "orediamond", "oreemerald", + "orequartz", "oresulfur", "oresaltpeter", "orenitre", "oreapatite", "orecertusquartz", + "orecharcoal", "oreamber", "oreobsidian")); + + /** + * Whether a block is a METAL ore, as far as the ore dictionary can say. Unknown blocks answer + * {@code false} — under-scaling leaves a world with the ore its climate gave it, while over-scaling + * would quietly rewrite a pack's non-metal deposits. + */ + static boolean isMetallic(IBlockState state) { + if (state == null || state.getBlock() == null) { + return false; + } + try { + net.minecraft.item.ItemStack stack = new net.minecraft.item.ItemStack(state.getBlock(), 1, + state.getBlock().getMetaFromState(state)); + for (int id : net.minecraftforge.oredict.OreDictionary.getOreIDs(stack)) { + String name = net.minecraftforge.oredict.OreDictionary.getOreName(id); + if (name == null) { + continue; + } + String lower = name.toLowerCase(java.util.Locale.ROOT); + if (lower.startsWith("ore") && !NON_METAL_ORES.contains(lower)) { + return true; + } + } + } catch (Throwable t) { + // No ore dictionary in this context (a headless derivation, or a block with no item form). + return false; + } + return false; + } + public static class OreEntry { int minHeight; int maxHeight; diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index 588da3818..6f3255c1c 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -28,6 +28,9 @@ import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.IGalaxyGenerator; +import zmaster587.advancedRocketry.universe.PlanetTypePreset; +import zmaster587.advancedRocketry.universe.PlanetTypes; +import zmaster587.advancedRocketry.universe.TerrainOption; import zmaster587.advancedRocketry.universe.UniverseRegistry; import net.minecraft.server.MinecraftServer; import net.minecraftforge.fml.common.FMLCommonHandler; @@ -61,6 +64,24 @@ public class XMLPlanetLoader { // between authored anchors; absent -> authored anchors only. All attrs are balance knobs with defaults. private static final String ELEMENT_GALAXYGEN = "galaxyGen"; private static final String ELEMENT_STARTYPE = "starType"; + // A planet TYPE preset: the named region of parameter space a world can land in, plus everything + // that follows from being that kind of world. Present -> replaces the whole stock table. + private static final String ELEMENT_PLANETTYPE = "planetType"; + private static final String ELEMENT_TYPE_PRESSURE = "pressure"; + private static final String ELEMENT_TYPE_TEMPERATURE = "temperature"; + private static final String ELEMENT_TYPE_GRAVITY = "gravity"; + private static final String ELEMENT_TYPE_TERRAIN = "terrain"; + private static final String ELEMENT_TYPE_GEN = "gen"; + private static final String ATTR_MIN = "min"; + private static final String ATTR_MAX = "max"; + private static final String ATTR_SOURCE = "source"; + private static final String ATTR_WORLDTYPE = "worldType"; + private static final String ATTR_TEMPLATE_PATH = "path"; + private static final String ATTR_GENTYPE = "genType"; + private static final String ATTR_OPTIONS = "options"; + private static final String ATTR_GASGIANT = "gasGiant"; + private static final String ATTR_ALLOWS_OXYGEN = "allowsOxygen"; + private static final String ATTR_TIDALLY_LOCKABLE = "tidallyLockable"; private static final String ATTR_DENSITY = "density"; private static final String ATTR_MINSPACING = "minSpacing"; private static final String ATTR_CLUSTERSCALE = "clusterScale"; @@ -92,6 +113,10 @@ public class XMLPlanetLoader { private static final String ELEMENT_FOGCOLOR = "fogColor"; private static final String ELEMENT_SKYCOLOR = "skyColor"; private static final String ELEMENT_GRAVITY = "gravitationalMultiplier"; + private static final String ELEMENT_MASS = "mass"; + private static final String ELEMENT_RADIUS = "radius"; + private static final String ELEMENT_TIDALLY_LOCKED = "tidallyLocked"; + private static final String ELEMENT_METALLICITY = "metallicity"; private static final String ELEMENT_DISTANCE = "orbitalDistance"; private static final String ELEMENT_BASEORBITTHETA = "orbitalTheta"; private static final String ELEMENT_PHI = "orbitalPhi"; @@ -234,6 +259,208 @@ private GalaxyGenConfig readGalaxyGen(Node node) { return new GalaxyGenConfig(density, minSpacing, clusterScale, voidFraction, types); } + /** + * Parse one {@code } element into a preset. + * + *
{@code
+     * 
+     *   
+     *   
+     *   
+     *   
+     *     
+     *     
+     *     
+     *   
+     *   advancedrocketry:moondark;10,minecraft:ice_flats;30
+     *   ...
+     * 
+     * }
+ * + *

Ranges are in the game's own units: pressure in atmosphere-density units (100 = 1 atm), + * temperature in KELVIN, gravity in percent of Earth's. Every attribute has a default, so a + * {@code } with nothing else is a valid (if very greedy) preset.

+ */ + private PlanetTypePreset readPlanetType(Node node) { + String name = attr(node, ATTR_NAME); + PlanetTypePreset.Builder b = PlanetTypePreset.builder(name == null ? "" : name) + .weight(attrInt(node, ATTR_WEIGHT, 10)) + .gasGiant(attrBool(node, ATTR_GASGIANT, false)) + .allowsOxygen(attrBool(node, ATTR_ALLOWS_OXYGEN, false)) + .tidallyLockable(attrBool(node, ATTR_TIDALLY_LOCKABLE, true)); + + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + String tag = child.getNodeName(); + if (ELEMENT_TYPE_PRESSURE.equalsIgnoreCase(tag)) { + b.pressure(attrInt(child, ATTR_MIN, DimensionProperties.MIN_ATM_PRESSURE), + attrInt(child, ATTR_MAX, DimensionProperties.MAX_ATM_PRESSURE)); + } else if (ELEMENT_TYPE_TEMPERATURE.equalsIgnoreCase(tag)) { + b.temperature(attrInt(child, ATTR_MIN, 0), attrInt(child, ATTR_MAX, 5000)); + } else if (ELEMENT_TYPE_GRAVITY.equalsIgnoreCase(tag)) { + b.gravity(attrInt(child, ATTR_MIN, DimensionProperties.MIN_GRAVITY), + attrInt(child, ATTR_MAX, DimensionProperties.MAX_GRAVITY)); + } else if (ELEMENT_TYPE_TERRAIN.equalsIgnoreCase(tag)) { + NodeList gens = child.getChildNodes(); + for (int j = 0; j < gens.getLength(); j++) { + Node gen = gens.item(j); + if (ELEMENT_TYPE_GEN.equalsIgnoreCase(gen.getNodeName())) { + b.terrain(new TerrainOption( + TerrainSource.byName(attr(gen, ATTR_SOURCE)), + attr(gen, ATTR_WORLDTYPE), + attr(gen, ATTR_TEMPLATE_PATH), + attrInt(gen, ATTR_GENTYPE, 0), + attr(gen, ATTR_OPTIONS), + attrInt(gen, ATTR_WEIGHT, 1))); + } + } + } else if (ELEMENT_BIOMEIDS.equalsIgnoreCase(tag)) { + b.biomes(child.getTextContent()); + } else if (ELEMENT_OREGEN.equalsIgnoreCase(tag)) { + b.ores(XMLOreLoader.loadOre(child)); + } else if (ELEMENT_SEALEVEL.equalsIgnoreCase(tag)) { + b.seaLevel(parseIntOr(child.getTextContent(), PlanetTypePreset.SEA_LEVEL_UNSET)); + } else if (ELEMENT_OCEANBLOCK.equalsIgnoreCase(tag)) { + b.oceanBlock(child.getTextContent()); + } + } + return b.build(); + } + + /** + * Apply an authored biome palette — the {@code } format — to a planet. + * + *

Public and shared because a planet TYPE declares its palette in exactly the same language a + * planet does, and a realized procedural world has to mean by it precisely what an authored world + * means. Two parsers for one format is two chances for a pack's entry to work in one place and be + * ignored in the other.

+ * + *

Format: comma-separated entries of {@code biome} or {@code biome;weight}, where {@code biome} + * is a registry name (preferred) or a raw numeric id (legacy, and modset-dependent). A malformed + * entry is warned about and skipped; it never aborts the rest of the list.

+ */ + public static void applyBiomeList(DimensionProperties properties, String authoredList) { + if (properties == null || authoredList == null || authoredList.trim().isEmpty()) { + return; + } + for (String s : authoredList.split(",")) { + if (s.trim().isEmpty()) { + continue; + } + int biomeWeight = 30; + String[] weightSplit = s.trim().split(";"); + + //Try to get a weight out of the semicolon separator + if (weightSplit.length > 1) { + try { + biomeWeight = Integer.parseInt(weightSplit[1].trim()); + if (biomeWeight == 0) { + AdvancedRocketry.logger.warn("Weight cannot be 0! Setting weight to default"); + biomeWeight = 30; + } + } catch (NumberFormatException e) { + biomeWeight = 30; + AdvancedRocketry.logger.warn(weightSplit[1] + " is not a valid biome weight"); + } + } + + //Check whether we have numeric IDs (bad!) or RL ids + ResourceLocation location = new ResourceLocation(weightSplit[0]); + if (Biome.REGISTRY.containsKey(location)) { + Biome biome = Biome.REGISTRY.getObject(location); + if (biome == null) + AdvancedRocketry.logger.warn("Error adding " + weightSplit[0]); //TODO: more detailed error msg + else + properties.addBiomeWeighted(biome, biomeWeight); + } else { + try { + int biome = Integer.parseInt(weightSplit[0]); + + if (!properties.addBiome(biome)) + AdvancedRocketry.logger.warn(weightSplit[0] + " is not a valid biome id"); //TODO: more detailed error msg + } catch (NumberFormatException e) { + AdvancedRocketry.logger.warn(weightSplit[0] + " is not a valid biome id or name"); //TODO: more detailed error msg + } + } + } + } + + private static boolean attrBool(Node node, String name, boolean def) { + String v = attr(node, name); + if (v == null || v.trim().isEmpty()) { + return def; + } + return Boolean.parseBoolean(v.trim()); + } + + private static int parseIntOr(String text, int def) { + if (text == null || text.trim().isEmpty()) { + return def; + } + try { + return Integer.parseInt(text.trim()); + } catch (NumberFormatException e) { + return def; + } + } + + /** Emit a preset so a re-read round-trips the authored table. */ + private static Element writePlanetType(Document doc, PlanetTypePreset preset) { + Element e = doc.createElement(ELEMENT_PLANETTYPE); + e.setAttribute(ATTR_NAME, preset.name()); + e.setAttribute(ATTR_WEIGHT, Integer.toString(preset.weight())); + if (preset.gasGiant()) { + e.setAttribute(ATTR_GASGIANT, "true"); + } + if (preset.allowsOxygen()) { + e.setAttribute(ATTR_ALLOWS_OXYGEN, "true"); + } + if (!preset.tidallyLockable()) { + e.setAttribute(ATTR_TIDALLY_LOCKABLE, "false"); + } + e.appendChild(range(doc, ELEMENT_TYPE_PRESSURE, preset.minPressure(), preset.maxPressure())); + e.appendChild(range(doc, ELEMENT_TYPE_TEMPERATURE, preset.minTemperature(), preset.maxTemperature())); + e.appendChild(range(doc, ELEMENT_TYPE_GRAVITY, preset.minGravity(), preset.maxGravity())); + Element terrain = doc.createElement(ELEMENT_TYPE_TERRAIN); + for (TerrainOption option : preset.terrain()) { + Element gen = doc.createElement(ELEMENT_TYPE_GEN); + gen.setAttribute(ATTR_SOURCE, option.source().name()); + if (!option.worldType().isEmpty()) { + gen.setAttribute(ATTR_WORLDTYPE, option.worldType()); + } + if (!option.template().isEmpty()) { + gen.setAttribute(ATTR_TEMPLATE_PATH, option.template()); + } + if (option.source() == TerrainSource.NATIVE) { + gen.setAttribute(ATTR_GENTYPE, Integer.toString(option.genType())); + } + if (!option.options().isEmpty()) { + gen.setAttribute(ATTR_OPTIONS, option.options()); + } + gen.setAttribute(ATTR_WEIGHT, Integer.toString(option.weight())); + terrain.appendChild(gen); + } + e.appendChild(terrain); + if (!preset.biomes().isEmpty()) { + e.appendChild(createTextNode(doc, ELEMENT_BIOMEIDS, preset.biomes())); + } + if (preset.seaLevel() != PlanetTypePreset.SEA_LEVEL_UNSET) { + e.appendChild(createTextNode(doc, ELEMENT_SEALEVEL, Integer.toString(preset.seaLevel()))); + } + if (!preset.oceanBlock().isEmpty()) { + e.appendChild(createTextNode(doc, ELEMENT_OCEANBLOCK, preset.oceanBlock())); + } + return e; + } + + private static Element range(Document doc, String tag, int min, int max) { + Element e = doc.createElement(tag); + e.setAttribute(ATTR_MIN, Integer.toString(min)); + e.setAttribute(ATTR_MAX, Integer.toString(max)); + return e; + } + private static Element writeGalaxyGen(Document doc, GalaxyGenConfig cfg) { Element e = doc.createElement(ELEMENT_GALAXYGEN); e.setAttribute(ATTR_DENSITY, Double.toString(cfg.density)); @@ -305,6 +532,12 @@ public static String writeXML(IGalaxy galaxy) { IGalaxyGenerator activeGenerator = UniverseRegistry.getGenerator(); if (activeGenerator instanceof ClusteredGalaxyGenerator) { galaxyElement.appendChild(writeGalaxyGen(doc, ((ClusteredGalaxyGenerator) activeGenerator).config())); + // The planet-type table travels with the generator, and only with it: an authored-anchors-only + // world has nothing that draws a type, so writing the presets there would put a section into + // the file that nothing reads. + for (PlanetTypePreset preset : PlanetTypes.presets()) { + galaxyElement.appendChild(writePlanetType(doc, preset)); + } } TransformerFactory transformerFactory = TransformerFactory.newInstance(); @@ -395,6 +628,19 @@ private static Node writePlanet(Document doc, DimensionProperties properties) { nodePlanet.appendChild(createTextNode(doc, ELEMENT_FOGCOLOR, properties.fogColor[0] + "," + properties.fogColor[1] + "," + properties.fogColor[2])); nodePlanet.appendChild(createTextNode(doc, ELEMENT_SKYCOLOR, properties.skyColor[0] + "," + properties.skyColor[1] + "," + properties.skyColor[2])); nodePlanet.appendChild(createTextNode(doc, ELEMENT_GRAVITY, (int) (properties.getGravitationalMultiplier() * 100f))); + // Bulk properties are written only when the planet HAS them, so a catalogue that never stated a + // mass round-trips to the same file it came from. + if (properties.hasBulkProperties()) { + nodePlanet.appendChild(createTextNode(doc, ELEMENT_MASS, Double.toString(properties.getMass()))); + nodePlanet.appendChild(createTextNode(doc, ELEMENT_RADIUS, Double.toString(properties.getRadius()))); + } + if (properties.isTidallyLocked()) { + nodePlanet.appendChild(createTextNode(doc, ELEMENT_TIDALLY_LOCKED, "true")); + } + if (properties.getMetallicity() != 1d) { + nodePlanet.appendChild(createTextNode(doc, ELEMENT_METALLICITY, + Double.toString(properties.getMetallicity()))); + } nodePlanet.appendChild(createTextNode(doc, ELEMENT_DISTANCE, properties.getOrbitalDist())); // Written as fractional degrees, not truncated to whole ones: these two angles are the only // authored inputs a body's durable CELL NAME is derived from, and one degree at a large @@ -779,9 +1025,36 @@ else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_ATMDENSITY)) try { properties.gravitationalMultiplier = Math.min(Math.max(Integer.parseInt(planetPropertyNode.getTextContent()), DimensionProperties.MIN_GRAVITY), DimensionProperties.MAX_GRAVITY) / 100f; + // Stating a gravity makes it an OVERRIDE: a planet that also declares a mass and a + // radius keeps the gravity its author wrote, so adding bulk properties to an + // existing planet cannot change how it plays. + properties.setGravityAuthored(true); } catch (NumberFormatException e) { AdvancedRocketry.logger.warn("Invalid gravitationalMultiplier specified"); //TODO: more detailed error msg } + } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_MASS)) { + try { + properties.setBulk(Double.parseDouble(planetPropertyNode.getTextContent()), + properties.getRadius()); + } catch (NumberFormatException e) { + AdvancedRocketry.logger.warn("Invalid mass specified for dimension " + properties.getId()); + } + } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_RADIUS)) { + try { + properties.setBulk(properties.getMass(), + Double.parseDouble(planetPropertyNode.getTextContent())); + } catch (NumberFormatException e) { + AdvancedRocketry.logger.warn("Invalid radius specified for dimension " + properties.getId()); + } + } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_TIDALLY_LOCKED)) { + properties.setTidallyLocked(Boolean.parseBoolean(planetPropertyNode.getTextContent())); + } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_METALLICITY)) { + try { + properties.setMetallicity(Double.parseDouble(planetPropertyNode.getTextContent())); + } catch (NumberFormatException e) { + AdvancedRocketry.logger.warn("Invalid metallicity specified for dimension " + + properties.getId()); + } } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_DISTANCE)) { try { @@ -839,46 +1112,7 @@ else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_TARGETSEALEVE else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_RIVER_OVERRIDE)) properties.hasRivers = Boolean.parseBoolean(planetPropertyNode.getTextContent()); else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_BIOMEIDS)) { - - String[] biomeList = planetPropertyNode.getTextContent().split(","); - for (String s : biomeList) { - - int biomeWeight = 30; - String[] weightSplit = s.split(";"); - - //Try to get a weight out of the semicolon separator - if (weightSplit.length > 1) { - try { - biomeWeight = Integer.parseInt(weightSplit[1]); - if (biomeWeight == 0) { - AdvancedRocketry.logger.warn("Weight cannot be 0! Setting weight to default"); - biomeWeight = 30; - } - } catch (NumberFormatException e) { - biomeWeight = 30; - AdvancedRocketry.logger.warn(weightSplit[1] + " is not a valid biome weight"); - } - } - - //Check whether we have numeric IDs (bad!) or RL ids - ResourceLocation location = new ResourceLocation(weightSplit[0]); - if (Biome.REGISTRY.containsKey(location)) { - Biome biome = Biome.REGISTRY.getObject(location); - if (biome == null) - AdvancedRocketry.logger.warn("Error adding " + weightSplit[0]); //TODO: more detailed error msg - else - properties.addBiomeWeighted(biome, biomeWeight); - } else { - try { - int biome = Integer.parseInt(weightSplit[0]); - - if (!properties.addBiome(biome)) - AdvancedRocketry.logger.warn(weightSplit[0] + " is not a valid biome id"); //TODO: more detailed error msg - } catch (NumberFormatException e) { - AdvancedRocketry.logger.warn(weightSplit[0] + " is not a valid biome id or name"); //TODO: more detailed error msg - } - } - } + applyBiomeList(properties, planetPropertyNode.getTextContent()); } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_CRATER_BIOMEIDS)) { String[] biomeList = planetPropertyNode.getTextContent().split(","); @@ -1322,6 +1556,11 @@ public DimensionPropertyCoupling readAllPlanets() { masterNode = masterNode.getNextSibling(); continue; } + if (masterNode.getNodeName().equalsIgnoreCase(ELEMENT_PLANETTYPE)) { + coupling.planetTypes.add(readPlanetType(masterNode)); + masterNode = masterNode.getNextSibling(); + continue; + } if (!masterNode.getNodeName().equals("star")) { masterNode = masterNode.getNextSibling(); continue; @@ -1398,6 +1637,8 @@ public static class DimensionPropertyCoupling { public Map anchorCoords = new HashMap<>(); // Procedural-galaxy generation config from an optional element; null = authored-only. public GalaxyGenConfig galaxyGenConfig = null; + // Authored presets. Empty -> the stock table stands. + public List planetTypes = new ArrayList<>(); } } diff --git a/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderPlanet.java b/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderPlanet.java index 290af794d..e917e6528 100644 --- a/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderPlanet.java +++ b/src/main/java/zmaster587/advancedRocketry/world/provider/WorldProviderPlanet.java @@ -515,9 +515,24 @@ public double getHorizon() { return 63; } + /** + * Where a tidally-locked world's sun sits, permanently. Zero is noon in vanilla's angle convention + * ({@code (time % period) / period - 0.25} is zero at midday), so a locked world stands under a sun + * that never sets. + * + *

One sky serves a whole dimension, so this expresses the half of tidal locking a per-dimension + * value CAN express — that there is no day/night cycle at all. The permanently-dark hemisphere and + * the temperate terminator strip between them are a property of WHERE you stand, which a single + * celestial angle has no way to say; they belong to the terrain and biome layer.

+ */ + private static final float TIDALLY_LOCKED_CELESTIAL_ANGLE = 0f; + @Override public float calculateCelestialAngle(long p_76563_1_, float p_76563_3_) { int rotationalPeriod; + if (getDimensionProperties(new BlockPos(0, 0, 0)).isTidallyLocked()) { + return TIDALLY_LOCKED_CELESTIAL_ANGLE; + } rotationalPeriod = getRotationalPeriod(new BlockPos(0, 0, 0)); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/ProceduralPlanetRealizationE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/ProceduralPlanetRealizationE2ETest.java new file mode 100644 index 000000000..a97ab29ce --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/server/ProceduralPlanetRealizationE2ETest.java @@ -0,0 +1,173 @@ +package zmaster587.advancedRocketry.test.server; + +import com.github.stannismod.forge.testing.junit.AbstractHeadlessServerTest; + +import org.junit.After; +import org.junit.Test; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +/** + * A procedural planet becomes somewhere you can stand, and it is the planet the scan described. + * + *

Before this batch the generator filled the galaxy with bodies carrying {@code INVALID_PLANET}, so + * {@code isDescendTarget()} was false for every one of them and a system full of planets had nowhere to + * land. This drives the real realization path on a real server and measures three things that are easy + * to claim and easy to get wrong:

+ * + *
    + *
  1. The scan and the landing agree. Mass, atmosphere, temperature, gravity and water are + * promised to a telescope from across the system, so the world that is minted has to MATERIALIZE + * those numbers rather than roll fresh ones. The test compares the realized dimension against the + * derivation's own answer, read before anything was minted — never against a literal it wrote + * itself, which would pass just as well if both sides were wrong together.
  2. + *
  3. Realization is idempotent. The trigger is a per-tick proximity check, so a second ask + * must reuse the world rather than mint another.
  4. + *
  5. The world is real. It loads, it has ground, and the body now advertises itself as a + * descent target — the flag every downstream consumer reads.
  6. + *
+ * + *

Per-method harness on purpose: this installs a procedural generator, which is a JVM-global, and a + * shared server would carry it into every class that ran after it.

+ */ +public class ProceduralPlanetRealizationE2ETest extends AbstractHeadlessServerTest { + + /** + * A tiny super-cell spacing. A system's anchor is seated in the middle band of its super-cell, so at + * the production spacing of 512 the nearest system is hundreds of cells away and a bounded probe + * sweep finds an empty universe. The spacing is a balance knob, and nothing here asserts one. + */ + private static final String GEN_INSTALL = "artest space gen-install 0.9 4 8 0.0 987654321"; + private static final int SWEEP_RADIUS = 8; + + private String exec(String cmd) throws Exception { + return String.join("\n", client().execute(cmd)); + } + + @After + public void restoreGenerator() throws Exception { + try { + exec("artest space gen-reset"); + } catch (Exception ignored) { + } + } + + @Test + public void aProceduralBodyBecomesTheWorldTheScanDescribed() throws Exception { + String installed = exec(GEN_INSTALL); + assertTrue("the procedural generator must install: " + installed, + installed.contains("\"ok\":true")); + + String found = exec("artest space find-procedural " + SWEEP_RADIUS); + assertTrue("a dense procedural galaxy must offer a landable body: " + found, + found.contains("\"ok\":true")); + String cell = jsonInt(found, "sx") + " " + jsonInt(found, "sy") + " " + jsonInt(found, "sz"); + assertTrue("the body must carry the orbit its physics is derived from: " + found, + jsonInt(found, "orbitalDist") > 0); + + // CONTROL. Nothing in that cell is a descent target yet — which is the defect this whole path + // exists to fix, and without measuring it first "descendTarget is true afterwards" would be a + // statement about a flag that might always have been true. + String before = exec("artest space cell-info " + cell); + assertTrue("cell-info must answer: " + before, before.contains("\"ok\":true")); + assertFalse("no procedural body may be a descent target before it is realized: " + before, + before.contains("\"descendTarget\":true")); + + // What the telescope would say, taken BEFORE anything is minted. + String scan = exec("artest space derived " + cell); + assertTrue("the derivation must answer for an unrealized body: " + scan, + scan.contains("\"ok\":true")); + + String realized = exec("artest space realize " + cell); + assertTrue("realization must mint a world: " + realized, realized.contains("\"ok\":true")); + int dim = jsonInt(realized, "dim"); + assertTrue("a realized dimension id must be real: " + realized, dim > 1); + + // The whole contract, field by field. Terrain is deliberately absent from this list: its tier + // is APPROACH, not TELESCOPE, so the design lets it settle later — but it is compared anyway + // because the derivation is the single origin of every one of these. + assertEquals("orbital distance must be materialized, not re-rolled: scan " + scan + + " vs world " + realized, jsonInt(scan, "orbitalDist"), jsonInt(realized, "orbitalDist")); + assertEquals("gravity must match the scan: " + scan + " vs " + realized, + jsonInt(scan, "gravity"), jsonInt(realized, "gravity")); + assertEquals("atmospheric pressure must match the scan: " + scan + " vs " + realized, + jsonInt(scan, "pressure"), jsonInt(realized, "pressure")); + assertEquals("temperature must match the scan: " + scan + " vs " + realized, + jsonInt(scan, "temperature"), jsonInt(realized, "temperature")); + assertEquals("a breathable atmosphere must match the scan: " + scan + " vs " + realized, + jsonBool(scan, "oxygen"), jsonBool(realized, "oxygen")); + assertEquals("tidal locking must match the scan: " + scan + " vs " + realized, + jsonBool(scan, "locked"), jsonBool(realized, "locked")); + assertEquals("mass must match the scan: " + scan + " vs " + realized, + jsonDouble(scan, "mass"), jsonDouble(realized, "mass"), 1e-6d); + assertEquals("radius must match the scan: " + scan + " vs " + realized, + jsonDouble(scan, "radius"), jsonDouble(realized, "radius"), 1e-6d); + assertEquals("the star's metallicity must reach the world: " + scan + " vs " + realized, + jsonDouble(scan, "metallicity"), jsonDouble(realized, "metallicity"), 1e-6d); + assertEquals("the terrain source drawn for the type must be the one fixed on the world: " + + scan + " vs " + realized, jsonString(scan, "terrainSource"), + jsonString(realized, "terrainSource")); + + // Gravity is DERIVED from the bulk properties, so the world must not merely carry a number that + // happens to match — the relation has to hold on the world itself. + double mass = jsonDouble(realized, "mass"); + double radius = jsonDouble(realized, "radius"); + assertTrue("a realized world must carry real bulk properties: " + realized, + mass > 0d && radius > 0d); + double expected = Math.max(0.05d, Math.min(4d, mass / (radius * radius))); + assertEquals("surface gravity must be M/R^2: " + realized, + expected * 100d, jsonInt(realized, "gravity"), 1.5d); + + assertTrue("the body must now advertise itself as a descent target: " + realized, + realized.contains("\"descendTarget\":true")); + assertTrue("a procedural system keeps its synthetic negative star id: " + realized, + jsonInt(realized, "starId") < 0); + + // Idempotency: the trigger is a per-tick proximity check, so asking again is the normal case. + String again = exec("artest space realize " + cell); + assertTrue("a second descent must succeed: " + again, again.contains("\"ok\":true")); + assertEquals("a second descent must REUSE the world, not mint another: " + again, + dim, jsonInt(again, "dim")); + + // And the world is a world: it loads, and it has ground rather than a column of air. + String loaded = exec("artest dim time " + dim); + assertFalse("the realized dimension must load: " + loaded, loaded.contains("\"error\"")); + String sample = exec("artest worldgen sample " + dim + " 0 0"); + assertFalse("the realized world must generate terrain: " + sample, sample.contains("\"error\"")); + assertNotEquals("a realized planet must have ground under its sky: " + sample, + "minecraft:air", jsonString(sample, "topBlock")); + } + + // ─── tiny JSON readers (the probe surface is flat JSON on purpose) ───────── + + private static int jsonInt(String json, String key) { + Matcher m = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:\\s*(-?\\d+)").matcher(json); + assertTrue("missing int '" + key + "' in " + json, m.find()); + return Integer.parseInt(m.group(1)); + } + + private static double jsonDouble(String json, String key) { + Matcher m = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:\\s*(-?[\\d.eE+-]+)") + .matcher(json); + assertTrue("missing number '" + key + "' in " + json, m.find()); + return Double.parseDouble(m.group(1)); + } + + private static boolean jsonBool(String json, String key) { + Matcher m = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:\\s*(true|false)").matcher(json); + assertTrue("missing boolean '" + key + "' in " + json, m.find()); + return Boolean.parseBoolean(m.group(1)); + } + + private static String jsonString(String json, String key) { + Matcher m = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json); + assertTrue("missing string '" + key + "' in " + json, m.find()); + return m.group(1); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index 7fe11b02d..4dab7b93e 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -339,9 +339,15 @@ public void proceduralBodiesGetTheirOwnCellsInsideTheSuperCell() { } @Test - public void tinySpacingDegeneratesConsistentlyIntoTheAnchorCell() { - // minSpacing=1: the super-cell IS one cell, so every body clamps into the anchor cell — degenerate - // but consistent (attribution still exact, nothing escapes the box). + public void tinySpacingDegeneratesIntoALoneStar() { + // minSpacing=1: the super-cell IS one cell, and the star already holds it. A second real body + // would have to share that cell, which at most one real body per cell forbids — so the system + // degenerates to its star alone. Degenerate but CONSISTENT: attribution stays exact, nothing + // escapes the box, and no cell ends up with two destinations in it. + // + // (Before the retinue gained a distinctness rule this read "every body clamps into the anchor + // cell", which was the same arrangement described from the other side — and describing it that + // way made the invariant violation sound like the intended behaviour.) ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator( new GalaxyGenConfig(0.9d, 1, 8, 0.0d, null)); boolean checkedAny = false; @@ -352,9 +358,10 @@ public void tinySpacingDegeneratesConsistentlyIntoTheAnchorCell() { continue; } checkedAny = true; - for (SystemBody body : gen.bodiesFor(SEED, c)) { - assertTrue("with s=1 every body stays in the anchor cell", body.name().sameCell(c)); - } + List bodies = gen.bodiesFor(SEED, c); + assertEquals("a one-cell neighbourhood can host exactly one real body", 1, bodies.size()); + assertEquals("and that body is the star", SystemBodyKind.STAR, bodies.get(0).kind()); + assertTrue("which holds the anchor cell", bodies.get(0).name().sameCell(c)); } assertTrue(checkedAny); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java new file mode 100644 index 000000000..491a93369 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java @@ -0,0 +1,508 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.After; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.dimension.TerrainSource; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.BodyProfile; +import zmaster587.advancedRocketry.universe.PlanetDerivation; +import zmaster587.advancedRocketry.universe.PlanetTypePreset; +import zmaster587.advancedRocketry.universe.PlanetTypes; +import zmaster587.advancedRocketry.universe.SystemBodyKind; +import zmaster587.advancedRocketry.universe.TerrainOption; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for the procedural planet derivation. Pure JUnit; no Minecraft bootstrap. + * + *

What is pinned here is what the design PROMISES, never the numbers that happen to deliver it: that + * the same {@code (seed, cell)} answers the same world twice, that a world always satisfies the type it + * was given, that zoning follows temperature rather than a table, that gravity is derived from mass and + * radius, and that a terrain generator no installed mod provides is dropped BEFORE the draw rather than + * after. Every balance constant is exercised as an input and none is asserted as an expected value.

+ */ +public class PlanetDerivationTest { + + private static final long SEED = 0xBEEF1234L; + + @After + public void restoreGlobals() { + // Both are process-wide seams; a test that installs one must not leak it into the next class. + PlanetTypes.resetToStock(); + PlanetTypes.setWorldTypeAvailability(null); + } + + private static GalacticCoord cell(long sx, long sy, long sz) { + return GalacticCoord.ofSectorLocal(sx, sy, sz, 0L, 0L, 0L); + } + + /** A star of the given archetype. Temperature is in Advanced Rocketry units: 100 = Sol. */ + private static StellarBody star(int temperature, float size) { + StellarBody s = new StellarBody(); + s.setTemperature(temperature); + s.setSize(size); + s.setId(-1); + s.setName("test"); + return s; + } + + private static StellarBody sol() { + return star(100, 1.0f); + } + + /** Every body of one system, as the generator would lay it out. */ + private static List system(long seed, GalacticCoord anchor, StellarBody s, int count) { + List out = new ArrayList<>(); + for (int i = 0; i < count; i++) { + int orbit = PlanetDerivation.orbitalDistanceOf(seed, anchor, i, count, s); + // One cell per body, as the placement guarantees; the exact cell is the generator's business, + // so a distinct synthetic one is enough to key the per-body draws. + out.add(PlanetDerivation.derive(seed, anchor, cell(anchor.sectorX() + i + 1, 0, 0), 0, s, + false, orbit)); + } + return out; + } + + // ─── Determinism ─────────────────────────────────────────────────────────── + + @Test + public void theSameCellAlwaysDerivesTheSameWorld() { + StellarBody s = sol(); + for (long x = -12; x <= 12; x++) { + GalacticCoord anchor = cell(x, 3, -1); + for (int i = 0; i < 6; i++) { + int orbit = PlanetDerivation.orbitalDistanceOf(SEED, anchor, i, 6, s); + BodyProfile a = PlanetDerivation.derive(SEED, anchor, cell(x, 3, i), 0, s, false, orbit); + BodyProfile b = PlanetDerivation.derive(SEED, anchor, cell(x, 3, i), 0, s, false, orbit); + assertEquals("type must be stable", a.typeName(), b.typeName()); + assertEquals("terrain must be stable", a.terrain(), b.terrain()); + assertEquals("mass must be stable", a.massEarths(), b.massEarths(), 0d); + assertEquals("radius must be stable", a.radiusEarths(), b.radiusEarths(), 0d); + assertEquals("pressure must be stable", a.pressure(), b.pressure()); + assertEquals("temperature must be stable", a.temperatureKelvin(), b.temperatureKelvin()); + assertEquals("oxygen must be stable", a.hasOxygen(), b.hasOxygen()); + assertEquals("locking must be stable", a.tidallyLocked(), b.tidallyLocked()); + } + } + } + + @Test + public void aBodysWorldIsKeyedOnItsCellNotOnItsPositionInTheList() { + // The property that makes a profile survive a pin: a body keeps its world when the system's body + // COUNT changes under it, because the draw is keyed on the durable cell name and not on an index. + StellarBody s = sol(); + GalacticCoord anchor = cell(4, 0, 0); + GalacticCoord body = cell(9, 1, 2); + BodyProfile inFive = PlanetDerivation.derive(SEED, anchor, body, 0, s, false, 140); + BodyProfile inTwelve = PlanetDerivation.derive(SEED, anchor, body, 0, s, false, 140); + assertEquals(inFive.typeName(), inTwelve.typeName()); + assertEquals(inFive.massEarths(), inTwelve.massEarths(), 0d); + } + + @Test + public void aMoonIsNotACopyOfThePlanetWhoseCellItShares() { + // A moon lives in its parent's cell by construction, so without the variant it would draw the + // parent's exact physics — the same mass, the same air, the same world twice. + StellarBody s = sol(); + GalacticCoord anchor = cell(0, 0, 0); + GalacticCoord shared = cell(5, 0, 0); + BodyProfile planet = PlanetDerivation.derive(SEED, anchor, shared, 0, s, false, 100); + BodyProfile moon = PlanetDerivation.derive(SEED, anchor, shared, 1, s, true, 100); + assertFalse("a moon must not inherit its parent's exact bulk", + planet.massEarths() == moon.massEarths() + && planet.radiusEarths() == moon.radiusEarths()); + assertEquals(SystemBodyKind.MOON, moon.kind()); + assertTrue("a moon is never a giant", moon.radiusEarths() < 1.5d); + } + + @Test + public void metallicityBelongsToTheStarAndIsSharedByEveryBodyOfItsSystem() { + GalacticCoord anchor = cell(7, -2, 5); + double first = PlanetDerivation.metallicityOf(SEED, anchor); + assertEquals(first, PlanetDerivation.metallicityOf(SEED, anchor), 0d); + assertTrue("metallicity must be a positive multiplier", first > 0d); + StellarBody s = sol(); + for (BodyProfile p : system(SEED, anchor, s, 6)) { + assertEquals("every body of a system shares its star's metallicity", first, p.metallicity(), 0d); + } + // Different systems must not all be metal-average, or the axis does nothing. + Set seen = new HashSet<>(); + for (long x = -30; x <= 30; x++) { + seen.add(PlanetDerivation.metallicityOf(SEED, cell(x, 0, 0))); + } + assertTrue("metallicity must genuinely vary between stars", seen.size() > 10); + } + + // ─── The type a world gets ───────────────────────────────────────────────── + + @Test + public void everyDerivedWorldSatisfiesTheTypeItWasGiven() { + // The admission ranges are the whole meaning of a type: a world outside its own preset's box + // would be a world whose scan describes something else. + int checked = 0; + for (long x = -20; x <= 20; x++) { + StellarBody s = starFor(x); + GalacticCoord anchor = cell(x, 0, 0); + for (BodyProfile p : system(SEED, anchor, s, 8)) { + assertNotNull("no world may be left without a type", p.preset()); + assertTrue(p + " does not satisfy its own preset " + p.preset(), + p.preset().admits(p.pressure(), p.temperatureKelvin(), p.gravityPercent(), + p.kind() == SystemBodyKind.GAS_GIANT)); + checked++; + } + } + assertTrue(checked > 300); + } + + @Test + public void theStockTableLeavesNoWorldUnclassified() { + // A gap in the preset coverage is an authoring bug, and this is the only place it is visible: + // an unclassified world still lands and still renders, so nothing else would ever notice. + List uncovered = new ArrayList<>(); + int total = 0; + for (long x = -25; x <= 25; x++) { + for (long z = -4; z <= 4; z++) { + StellarBody s = starFor(x + z); + GalacticCoord anchor = cell(x, 0, z); + for (BodyProfile p : system(SEED, anchor, s, 8)) { + total++; + if (PlanetTypes.UNCLASSIFIED.equals(p.typeName()) && uncovered.size() < 15) { + uncovered.add("p=" + p.pressure() + " T=" + p.temperatureKelvin() + " g=" + + p.gravityPercent() + (p.kind() == SystemBodyKind.GAS_GIANT + ? " GIANT" : "")); + } + } + } + } + assertTrue("sample must be large", total > 2000); + // The message NAMES the gap: a bare count would say a hole exists without saying where, and the + // whole value of this test is that it hands the author the range to widen. + assertTrue("the stock presets must cover every world the derivation can produce; uncovered " + + "samples: " + uncovered, uncovered.isEmpty()); + } + + @Test + public void aWideRangeOfWorldsIsProduced() { + // The point of deriving a type rather than drawing one is variety that FOLLOWS the physics; a + // table that collapses onto one or two names would satisfy every other test here. + Set names = new HashSet<>(); + for (long x = -25; x <= 25; x++) { + StellarBody s = starFor(x); + for (BodyProfile p : system(SEED, cell(x, 1, 1), s, 8)) { + names.add(p.typeName()); + } + } + assertTrue("the derivation must produce many kinds of world, saw " + names, + names.size() >= 6); + } + + // ─── Zoning emerges from the physics ─────────────────────────────────────── + + @Test + public void aFartherOrbitIsAlwaysColder() { + StellarBody s = sol(); + int previous = Integer.MAX_VALUE; + for (int d = 10; d <= 4000; d += 10) { + int t = PlanetDerivation.bareTemperature(s, d); + assertTrue("temperature must never rise with distance (" + d + ")", t <= previous); + previous = t; + } + } + + @Test + public void giantsFormInTheColdAndNeverInTheHeat() { + // The zoning claim, stated in physics rather than in the implementation's threshold: a world + // warm enough for liquid water on its surface did not accrete a gas envelope. + int giantsCold = 0; + int checkedHot = 0; + for (long x = -30; x <= 30; x++) { + StellarBody s = starFor(x); + GalacticCoord anchor = cell(x, 2, 0); + for (BodyProfile p : system(SEED, anchor, s, 8)) { + boolean giant = p.kind() == SystemBodyKind.GAS_GIANT; + int bare = PlanetDerivation.bareTemperature(s, p.orbitalDistance()); + if (bare >= 273) { + checkedHot++; + assertFalse("a giant must not form above the freezing point of water: " + p, giant); + } else if (giant) { + giantsCold++; + } + } + } + assertTrue("the hot zone must actually be sampled", checkedHot > 100); + assertTrue("giants must actually form in the cold", giantsCold > 5); + } + + @Test + public void aColdSystemsWarmZoneSitsCloserInThanAHotOnes() { + // The reference distance is what makes "the warm zone" mean the same thing around every star. + int coolDwarf = PlanetDerivation.referenceDistance(star(40, 0.6f)); + int sunlike = PlanetDerivation.referenceDistance(sol()); + int blueGiant = PlanetDerivation.referenceDistance(star(220, 2.6f)); + assertTrue("a cool dwarf's warm zone must be inside a sunlike star's", coolDwarf < sunlike); + assertTrue("a hot star's warm zone must be outside a sunlike star's", blueGiant > sunlike); + } + + // ─── Gravity is DERIVED, and mass/radius are primary ─────────────────────── + + @Test + public void gravityFollowsMassOverRadiusSquared() { + StellarBody s = sol(); + for (long x = -20; x <= 20; x++) { + for (BodyProfile p : system(SEED, cell(x, 5, 5), s, 8)) { + double expected = p.massEarths() / (p.radiusEarths() * p.radiusEarths()); + double clamped = Math.max(0.05d, Math.min(4d, expected)); + assertEquals("gravity must be M/R^2 (clamped), not an independent draw", + clamped * 100d, p.gravityPercent(), 1.0d); + } + } + } + + @Test + public void doublingMassDoublesGravityAndDoublingRadiusQuartersIt() { + assertEquals(2d * zmaster587.advancedRocketry.dimension.DimensionProperties.derivedGravity(1d, 1d), + zmaster587.advancedRocketry.dimension.DimensionProperties.derivedGravity(2d, 1d), 1e-9d); + assertEquals(zmaster587.advancedRocketry.dimension.DimensionProperties.derivedGravity(1d, 1d) / 4d, + zmaster587.advancedRocketry.dimension.DimensionProperties.derivedGravity(1d, 2d), 1e-9d); + } + + // ─── Atmosphere retention ────────────────────────────────────────────────── + + @Test + public void aHeavierWorldHoldsMoreAirThanALighterOneInTheSameOrbit() { + // Retention is the physical claim behind the pressure draw; the scatter must not be big enough + // to reverse it across a large sample, or "heavy worlds have thick air" is not a rule at all. + StellarBody s = sol(); + double lightAverage = 0d; + double heavyAverage = 0d; + int light = 0; + int heavy = 0; + for (long x = -40; x <= 40; x++) { + for (BodyProfile p : system(SEED, cell(x, 9, 9), s, 8)) { + if (p.kind() == SystemBodyKind.GAS_GIANT) { + continue; + } + if (p.massEarths() < 0.3d) { + lightAverage += p.pressure(); + light++; + } else if (p.massEarths() > 3d) { + heavyAverage += p.pressure(); + heavy++; + } + } + } + assertTrue("both weight classes must be sampled", light > 20 && heavy > 20); + assertTrue("a heavy world must hold more air on average (" + (lightAverage / light) + " vs " + + (heavyAverage / heavy) + ")", + heavyAverage / heavy > lightAverage / light); + } + + // ─── Oxygen is biology, on top of an already-suitable world ──────────────── + + @Test + public void oxygenOnlyAppearsOnTypesThatPermitItAndStaysRare() { + int oxygen = 0; + int total = 0; + for (long x = -40; x <= 40; x++) { + StellarBody s = starFor(x); + for (BodyProfile p : system(SEED, cell(x, 7, 0), s, 8)) { + total++; + if (p.hasOxygen()) { + oxygen++; + assertTrue("oxygen on a type that forbids it: " + p, p.preset().allowsOxygen()); + } + } + } + assertTrue("sample must be large", total > 500); + assertTrue("a breathable world must stay rare, saw " + oxygen + "/" + total, + oxygen * 20 < total); + } + + // ─── Tidal locking ───────────────────────────────────────────────────────── + + @Test + public void aCloseOrbitIsLockedAndADistantOneIsNot() { + StellarBody s = sol(); + assertTrue("a very close orbit must be locked", PlanetDerivation.tidallyLockedAt(s, 1)); + assertFalse("a distant orbit must not be locked", PlanetDerivation.tidallyLockedAt(s, 100_000)); + } + + @Test + public void aCoolDwarfsWarmZoneLiesInsideItsLockingRadius() { + // The astronomical point of D4b, stated as the relation it rests on: around the commonest kind + // of star, the orbits that are warm enough to live in are also the ones that are locked — while + // around a sunlike star they are not. + StellarBody dwarf = star(40, 0.6f); + StellarBody sun = sol(); + assertTrue("a red dwarf's warm zone must be tidally locked", + PlanetDerivation.tidallyLockedAt(dwarf, PlanetDerivation.referenceDistance(dwarf))); + assertFalse("a sunlike star's warm zone must not be", + PlanetDerivation.tidallyLockedAt(sun, PlanetDerivation.referenceDistance(sun))); + } + + @Test + public void aGiantIsNeverReportedAsTidallyLocked() { + for (long x = -30; x <= 30; x++) { + StellarBody s = starFor(x); + for (BodyProfile p : system(SEED, cell(x, 11, 0), s, 8)) { + if (p.kind() == SystemBodyKind.GAS_GIANT) { + assertFalse("nobody stands on a giant, so locking it means nothing: " + p, + p.tidallyLocked()); + } + } + } + } + + // ─── Orbits ──────────────────────────────────────────────────────────────── + + @Test + public void orbitsAreOrderedAndSpreadLogarithmically() { + StellarBody s = sol(); + GalacticCoord anchor = cell(2, 2, 2); + int count = 9; + int previous = 0; + List orbits = new ArrayList<>(); + for (int i = 0; i < count; i++) { + int d = PlanetDerivation.orbitalDistanceOf(SEED, anchor, i, count, s); + assertTrue("body " + i + " must orbit outside body " + (i - 1) + " (" + previous + " -> " + + d + ")", d > previous); + previous = d; + orbits.add(d); + } + // Geometric spacing: the outer gaps must dwarf the inner ones, which uniform spacing never does. + int innerGap = orbits.get(1) - orbits.get(0); + int outerGap = orbits.get(count - 1) - orbits.get(count - 2); + assertTrue("spacing must widen outward (" + innerGap + " vs " + outerGap + ")", + outerGap > innerGap * 3); + } + + @Test + public void theCellPlacementFractionAgreesWithTheOrbitItCameFrom() { + // The placement maps an orbit onto a cell radius through this fraction, so a body that is third + // from its star is third out from the anchor cell. If the two ever disagreed, the sky would show + // a system laid out differently from the one the physics describes. + StellarBody s = sol(); + double previous = -1d; + for (int d = 1; d <= 20_000; d += 37) { + double f = PlanetDerivation.orbitFraction(d, s); + assertTrue("fraction must stay in [0,1]", f >= 0d && f <= 1d); + assertTrue("fraction must not decrease as the orbit grows", f >= previous); + previous = f; + } + } + + // ─── D6: the availability filter runs BEFORE the draw ────────────────────── + + @Test + public void anUnavailableWorldTypeIsDroppedAndItsWeightRedistributed() { + PlanetTypePreset preset = PlanetTypePreset.builder("t") + .terrain(TerrainOption.ofWorldType("MISSING", "", 97)) + .terrain(TerrainOption.ofNative(3, 2)) + .terrain(TerrainOption.ofTemplate("ruins", 1)) + .build(); + PlanetTypes.setWorldTypeAvailability(name -> false); + + Map drawn = new HashMap<>(); + for (int i = 0; i < 4000; i++) { + TerrainOption option = PlanetTypes.drawTerrain(preset, i * 0x9E3779B97F4A7C15L); + String key = option.source() + ":" + option.genType() + option.template(); + drawn.merge(key, 1, Integer::sum); + } + assertFalse("a world type no mod provides must never be drawn", + drawn.containsKey(TerrainSource.MOD_WORLDTYPE + ":0")); + // The survivors keep their RATIO to each other (2:1). Converting the missing entry's share into + // the native fallback instead would swamp the template at roughly 99:1. + int nativeDraws = drawn.getOrDefault(TerrainSource.NATIVE + ":3", 0); + int templateDraws = drawn.getOrDefault(TerrainSource.TEMPLATE + ":0ruins", 0); + assertTrue("both survivors must be drawn", nativeDraws > 0 && templateDraws > 0); + double ratio = nativeDraws / (double) templateDraws; + assertTrue("weights must renormalize among the survivors, not collapse into the fallback " + + "(saw " + nativeDraws + ":" + templateDraws + ")", + ratio > 1.5d && ratio < 2.5d); + } + + @Test + public void anAvailableWorldTypeIsDrawnNormally() { + PlanetTypePreset preset = PlanetTypePreset.builder("t") + .terrain(TerrainOption.ofWorldType("PRESENT", "opts", 99)) + .terrain(TerrainOption.ofNative(0, 1)) + .build(); + PlanetTypes.setWorldTypeAvailability(name -> "PRESENT".equals(name)); + int foreign = 0; + for (int i = 0; i < 500; i++) { + if (PlanetTypes.drawTerrain(preset, i * 0x9E3779B97F4A7C15L).source() + == TerrainSource.MOD_WORLDTYPE) { + foreign++; + } + } + assertTrue("an installed generator must dominate at weight 99:1, saw " + foreign + "/500", + foreign > 400); + } + + @Test + public void aPresetWhoseEveryGeneratorIsMissingStillProducesATerrain() { + PlanetTypePreset preset = PlanetTypePreset.builder("t") + .terrain(TerrainOption.ofWorldType("A", "", 1)) + .terrain(TerrainOption.ofWorldType("B", "", 1)) + .build(); + PlanetTypes.setWorldTypeAvailability(name -> false); + TerrainOption option = PlanetTypes.drawTerrain(preset, 12345L); + assertEquals("a world must still generate when its type's mods are all absent", + TerrainSource.NATIVE, option.source()); + } + + // ─── Type overlap is a weighted draw, not first match ────────────────────── + + @Test + public void overlappingPresetsShareTheirProbabilityByWeight() { + List table = new ArrayList<>(); + table.add(PlanetTypePreset.builder("common").weight(90) + .pressure(0, 1000).temperature(0, 1000).gravity(0, 400).build()); + table.add(PlanetTypePreset.builder("rare").weight(10) + .pressure(0, 1000).temperature(0, 1000).gravity(0, 400).build()); + PlanetTypes.setPresets(table); + + Map counts = new HashMap<>(); + for (int i = 0; i < 5000; i++) { + PlanetTypePreset p = PlanetTypes.drawType(100, 280, 100, false, + i * 0x9E3779B97F4A7C15L); + counts.merge(p.name(), 1, Integer::sum); + } + assertTrue("both overlapping presets must be reachable — first match would never draw the " + + "second: " + counts, + counts.getOrDefault("rare", 0) > 100); + assertTrue("the heavier preset must dominate: " + counts, + counts.getOrDefault("common", 0) > counts.getOrDefault("rare", 0) * 3); + } + + @Test + public void aWorldNoPresetAdmitsIsReportedRatherThanSubstituted() { + List table = new ArrayList<>(); + table.add(PlanetTypePreset.builder("narrow").weight(1) + .pressure(0, 10).temperature(0, 10).gravity(0, 10).build()); + PlanetTypes.setPresets(table); + assertEquals("silently substituting a preset would hide the coverage gap for ever", + null, PlanetTypes.drawType(900, 900, 300, false, 1L)); + } + + /** A star archetype that varies across the sweep, so no test measures one kind of system only. */ + private static StellarBody starFor(long x) { + int[] temps = {40, 70, 100, 150, 220}; + float[] sizes = {0.6f, 0.9f, 1.1f, 1.4f, 2.2f}; + int i = (int) Math.floorMod(x, temps.length); + return star(temps[i], sizes[i]); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java new file mode 100644 index 000000000..fdec9910c --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java @@ -0,0 +1,248 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.After; +import org.junit.Test; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import zmaster587.advancedRocketry.api.Constants; +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.SystemBody; +import zmaster587.advancedRocketry.universe.SystemBodyKind; +import zmaster587.advancedRocketry.universe.UniverseRegistry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for the registry half of realization — the half that decides whether a body has a + * world, and therefore the half that has to be idempotent. + * + *

Minting the dimension itself needs a live server and is pinned by the server e2e. What is pinned + * HERE is the property that makes minting safe to drive from a per-tick proximity check: asking twice + * gives the same answer, and a body that already has a world is never handed a second one. If that ever + * stopped holding, a pilot hovering at the descent boundary would allocate a dimension per tick.

+ */ +public class PlanetRealizationTest { + + private static final long SEED = 0x5EED5EEDL; + + @After + public void resetSeams() { + UniverseRegistry.setGenerator(null); + UniverseRegistry.setStarLookup(null); + } + + /** + * A dense, void-free galaxy so a small sweep is guaranteed to find systems. The spacing is + * deliberately tiny: a system's anchor is seated in the MIDDLE BAND of its super-cell, so at the + * production spacing of 512 the nearest anchor is hundreds of cells from the origin and a + * unit-test-sized sweep finds an empty universe. + */ + private static UniverseRegistry registryWithProceduralGalaxy() { + UniverseRegistry reg = new UniverseRegistry(); + UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator( + new GalaxyGenConfig(0.9d, 4, 8, 0.0d, null))); + reg.bindWorldSeed(SEED); + return reg; + } + + /** The first cell in a small sweep holding a body a ship could land on but that has no world. */ + private static GalacticCoord findLandableCell(UniverseRegistry reg) { + for (long x = -8; x <= 8; x++) { + for (long y = -8; y <= 8; y++) { + for (long z = -8; z <= 8; z++) { + GalacticCoord cell = GalacticCoord.ofSectorLocal(x, y, z, 0L, 0L, 0L); + for (SystemBody b : reg.bodiesAt(cell)) { + if (b.kind().canDescend() && b.dimId() == Constants.INVALID_PLANET) { + return cell; + } + } + } + } + } + return null; + } + + @Test + public void theProceduralGalaxyOffersLandableBodiesThatHaveNoWorldYet() { + // The precondition of everything below, and the defect the whole batch exists to fix: the + // generator places bodies a ship could stand on, and not one of them is a descent target. + UniverseRegistry reg = registryWithProceduralGalaxy(); + GalacticCoord cell = findLandableCell(reg); + assertNotNull("a dense procedural galaxy must contain landable bodies", cell); + for (SystemBody b : reg.bodiesAt(cell)) { + if (b.kind().canDescend()) { + assertFalse("an unrealized body must not advertise itself as a descent target", + b.isDescendTarget()); + } + } + } + + @Test + public void realizingABodyMakesItADescentTargetAndRecordsItsCellName() { + UniverseRegistry reg = registryWithProceduralGalaxy(); + GalacticCoord cell = findLandableCell(reg); + assertNotNull(cell); + + assertTrue("touching a procedural system must pin it before anything is written into it", + reg.pinSystem(cell)); + assertTrue("the pinned body must accept a dimension", reg.realizeBody(cell, 4242)); + + OptionalInt realized = reg.realizedDimAt(cell); + assertTrue("the cell must now report a realized world", realized.isPresent()); + assertEquals(4242, realized.getAsInt()); + + boolean sawTarget = false; + for (SystemBody b : reg.bodiesAt(cell)) { + if (b.dimId() == 4242) { + assertTrue("a realized body must be a descent target", b.isDescendTarget()); + sawTarget = true; + } + } + assertTrue(sawTarget); + + assertEquals("the body's cell must be recorded as that dimension's durable name", + Optional.of(cell.cellCentre()), reg.recordedName(4242)); + } + + @Test + public void asecondDescentIntoTheSameCellReusesTheWorld() { + // The idempotency contract. The trigger is a per-tick proximity check, so "ask again" is the + // normal case, not an edge one — a pilot who hovers at the boundary must not mint a dimension + // per tick. + UniverseRegistry reg = registryWithProceduralGalaxy(); + GalacticCoord cell = findLandableCell(reg); + assertNotNull(cell); + reg.pinSystem(cell); + assertTrue(reg.realizeBody(cell, 777)); + + assertEquals("asking again must answer the SAME world", 777, + reg.realizedDimAt(cell).getAsInt()); + assertTrue("re-realizing with the same id is a no-op, not a failure", + reg.realizeBody(cell, 777)); + assertEquals(777, reg.realizedDimAt(cell).getAsInt()); + } + + @Test + public void aBodyThatAlreadyHasAWorldRefusesASecondOne() { + UniverseRegistry reg = registryWithProceduralGalaxy(); + GalacticCoord cell = findLandableCell(reg); + assertNotNull(cell); + reg.pinSystem(cell); + assertTrue(reg.realizeBody(cell, 100)); + + assertFalse("a body must never be re-pointed at a different world", reg.realizeBody(cell, 200)); + assertEquals("and it must still hold the first one", 100, reg.realizedDimAt(cell).getAsInt()); + } + + @Test + public void anUnpinnedSystemCannotBeRealizedIntoAtAll() { + // Not a limitation but the mechanism: a derived body list is regenerated on the next query, so + // writing a dimension into one would be writing into a value that is about to be thrown away. + UniverseRegistry reg = registryWithProceduralGalaxy(); + GalacticCoord cell = findLandableCell(reg); + assertNotNull(cell); + assertFalse("an unpinned system must refuse the rewrite rather than lose it silently", + reg.realizeBody(cell, 55)); + assertFalse(reg.realizedDimAt(cell).isPresent()); + } + + @Test + public void aPinnedSystemsStarSurvivesAChangeOfGenerator() { + // Realization derives a body's physics from its STAR, so the star a landing uses has to be the + // one the scan described — even after a config edit that would have fabricated a different one. + UniverseRegistry reg = registryWithProceduralGalaxy(); + GalacticCoord cell = findLandableCell(reg); + assertNotNull(cell); + reg.pinSystem(cell); + + Optional before = reg.starAt(cell); + assertTrue("a pinned system must have a star", before.isPresent()); + + // A pack edit: a different spacing, a different density, a whole different galaxy. + UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator( + new GalaxyGenConfig(0.2d, 32, 4, 0.5d, null))); + + Optional after = reg.starAt(cell); + assertTrue(after.isPresent()); + assertEquals("a pinned star's identity must not move", before.get().getId(), + after.get().getId()); + assertEquals("nor its temperature", before.get().getTemperature(), after.get().getTemperature()); + assertEquals("nor its size", before.get().getSize(), after.get().getSize(), 0f); + } + + @Test + public void aRealizedBodyKeepsItsCellItsOrbitAndItsKind() { + // Realization materializes what was derived; it must not MOVE the body. An address a player + // wrote down before landing has to keep denoting the world they landed on. + UniverseRegistry reg = registryWithProceduralGalaxy(); + GalacticCoord cell = findLandableCell(reg); + assertNotNull(cell); + + SystemBody before = null; + for (SystemBody b : reg.bodiesAt(cell)) { + if (b.kind().canDescend()) { + before = b; + break; + } + } + assertNotNull(before); + reg.pinSystem(cell); + assertTrue(reg.realizeBody(cell, 999)); + + SystemBody after = null; + for (SystemBody b : reg.bodiesAt(cell)) { + if (b.dimId() == 999) { + after = b; + break; + } + } + assertNotNull(after); + assertEquals("the cell name must not move", before.name(), after.name()); + assertEquals("the orbit must not move", before.orbitalDistance(), after.orbitalDistance()); + assertEquals("the kind must not change", before.kind(), after.kind()); + assertEquals("the owning system must not change", before.starId(), after.starId()); + assertNotEquals("but it must now have a world", before.dimId(), after.dimId()); + } + + @Test + public void aProceduralBodyCarriesTheOrbitItsPhysicsWasDerivedFrom() { + // The orbit travels ON the body so a pinned system's worlds stay derivable after any change to + // the placement arithmetic. A body with no orbit would have no climate. + UniverseRegistry reg = registryWithProceduralGalaxy(); + GalacticCoord cell = findLandableCell(reg); + assertNotNull(cell); + List here = reg.bodiesAt(cell); + boolean checked = false; + for (SystemBody b : here) { + if (b.kind() == SystemBodyKind.STAR) { + continue; + } + assertTrue("a procedural body must carry a real orbital distance, got " + + b.orbitalDistance(), b.orbitalDistance() > 0); + checked = true; + } + assertTrue(checked); + } + + @Test + public void theOrbitSurvivesAnNbtRoundTrip() { + SystemBody body = new SystemBody(GalacticCoord.ofSectorLocal(3, 4, 5, 0, 0, 0), + SystemBodyKind.PLANET, 12, -7, 1234); + net.minecraft.nbt.NBTTagCompound nbt = new net.minecraft.nbt.NBTTagCompound(); + body.writeToNBT(nbt); + SystemBody back = SystemBody.readFromNBT(nbt); + assertEquals("a pinned body's orbit must survive the save, or its world is not re-derivable", + 1234, back.orbitalDistance()); + assertEquals(body, back); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java new file mode 100644 index 000000000..7064dfb2b --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java @@ -0,0 +1,340 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.After; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.PlanetTypes; +import zmaster587.advancedRocketry.universe.SystemBody; +import zmaster587.advancedRocketry.universe.SystemBodyKind; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for a system's RETINUE — how many bodies it has, where they sit, and what it always + * contains. + * + *

The shape is what is pinned, never the constants that produce it: a long-tailed body count rather + * than a fixed ceiling, an outer belt on every system without exception, moons living inside their + * parent's cell, and — the one that is not cosmetic — no two real bodies sharing a cell. A cell + * is the unit a jump is aimed at and the unit a ship arrives into, so two real bodies in one are two + * destinations a player can neither tell apart nor choose between.

+ */ +public class SystemRetinueTest { + + private static final long SEED = 0xA57E401DL; + + @After + public void restoreGlobals() { + PlanetTypes.resetToStock(); + PlanetTypes.setWorldTypeAvailability(null); + } + + private static GalacticCoord cell(long sx, long sy, long sz) { + return GalacticCoord.ofSectorLocal(sx, sy, sz, 0L, 0L, 0L); + } + + /** + * A galaxy dense enough to sample and roomy enough to lay a system out in. The spacing has to leave + * a real neighbourhood: a body's cell radius is bounded by {@code 3s/8}, so at {@code s=4} a system + * has a single ring of cells to put a dozen bodies in. + */ + private static ClusteredGalaxyGenerator gen(int minSpacing) { + return new ClusteredGalaxyGenerator(new GalaxyGenConfig(0.9d, minSpacing, 8, 0.0d, null)); + } + + /** Every occupied system anchor in a sweep of super-cells. */ + private static List anchors(ClusteredGalaxyGenerator g, long seed, int minSpacing, + int supercells) { + Set seen = new HashSet<>(); + List out = new ArrayList<>(); + for (long sx = -supercells; sx <= supercells; sx++) { + for (long sy = -supercells; sy <= supercells; sy++) { + for (long sz = -supercells; sz <= supercells; sz++) { + Optional a = g.anchorAt(seed, + cell(sx * minSpacing, sy * minSpacing, sz * minSpacing)); + if (a.isPresent() && seen.add(a.get().cellKey())) { + out.add(a.get()); + } + } + } + } + return out; + } + + // ─── The invariant the audit exists to protect ───────────────────────────── + + @Test + public void noTwoRealBodiesOfOneSystemShareACell() { + // Measured the way SystemContent.auditOneRealBodyPerCell measures it — moons exempt, because a + // moon lives in its parent's cell by construction — so the generator and the audit cannot + // disagree silently about what the invariant says. + int minSpacing = 64; + ClusteredGalaxyGenerator g = gen(minSpacing); + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { + Map perCell = new HashMap<>(); + for (SystemBody b : g.bodiesFor(SEED, anchor)) { + if (b.kind() == SystemBodyKind.MOON) { + continue; + } + perCell.merge(b.name().cellKey(), 1, Integer::sum); + } + for (Map.Entry e : perCell.entrySet()) { + assertEquals("system " + anchor.cellKey() + " put " + e.getValue() + + " real bodies in cell " + e.getKey(), 1, (int) e.getValue()); + } + checked++; + } + assertTrue("the sweep must actually find systems", checked > 5); + } + + @Test + public void theInvariantHoldsEvenWhenTheNeighbourhoodIsCrampedForRoom() { + // The collision risk grows with the square of the body count, so the tightest spacing that still + // has more than one cell is where it bites. A cramped system is allowed to hold FEWER bodies; + // it is not allowed to hold two in one cell. + int minSpacing = 8; + ClusteredGalaxyGenerator g = gen(minSpacing); + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { + Set cells = new HashSet<>(); + for (SystemBody b : g.bodiesFor(SEED, anchor)) { + if (b.kind() == SystemBodyKind.MOON) { + continue; + } + assertTrue("cell " + b.name().cellKey() + " of system " + anchor.cellKey() + + " holds a second real body", cells.add(b.name().cellKey())); + } + checked++; + } + assertTrue(checked > 5); + } + + // ─── E1: a long-tailed body count ────────────────────────────────────────── + + @Test + public void systemSizeIsLongTailedRatherThanCapped() { + List counts = new ArrayList<>(); + for (long x = -400; x <= 400; x++) { + counts.add(ClusteredGalaxyGenerator.retinueSize(SEED, cell(x, 0, 0))); + } + Collections.sort(counts); + int median = counts.get(counts.size() / 2); + int biggest = counts.get(counts.size() - 1); + int smallest = counts.get(0); + + assertTrue("an ordinary system must be a handful of bodies, saw a median of " + median, + median >= 4 && median <= 8); + assertTrue("a rare system must be genuinely large — a find, not just a bit bigger; biggest " + + "seen was " + biggest, biggest >= 15); + assertTrue("and no system may be empty", smallest >= 1); + // The tail must be a TAIL: large systems rare, not a second mode. + int large = 0; + for (int c : counts) { + if (c >= 12) { + large++; + } + } + assertTrue("large systems must stay rare, saw " + large + "/" + counts.size(), + large * 10 < counts.size()); + assertTrue("but they must exist at all", large > 0); + } + + @Test + public void theRetinueSizeIsDeterministic() { + for (long x = -50; x <= 50; x++) { + GalacticCoord c = cell(x, 7, -3); + assertEquals(ClusteredGalaxyGenerator.retinueSize(SEED, c), + ClusteredGalaxyGenerator.retinueSize(SEED, c)); + } + } + + // ─── E3: every system has an outer belt ──────────────────────────────────── + + @Test + public void everySystemEndsInABelt() { + // Load-bearing beyond this task: drifting out of jump range is only survivable because every + // system has something to mine without landing. "Usually" would be a soft-lock. + int minSpacing = 64; + ClusteredGalaxyGenerator g = gen(minSpacing); + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { + List bodies = g.bodiesFor(SEED, anchor); + int belts = 0; + int outermostMajor = 0; + int outermostBelt = 0; + for (SystemBody b : bodies) { + if (b.kind() == SystemBodyKind.ASTEROID_BELT) { + belts++; + outermostBelt = Math.max(outermostBelt, b.orbitalDistance()); + } else if (b.kind() != SystemBodyKind.STAR && b.kind() != SystemBodyKind.MOON) { + outermostMajor = Math.max(outermostMajor, b.orbitalDistance()); + } + } + assertTrue("system " + anchor.cellKey() + " has no belt at all", belts >= 1); + assertTrue("the outermost body of a system must be a belt (major " + outermostMajor + + ", belt " + outermostBelt + ")", outermostBelt > outermostMajor); + checked++; + } + assertTrue(checked > 5); + } + + @Test + public void anInnerBeltAppearsOnlyWhereAGiantClearedOne() { + // A belt is material a giant's resonances stopped from accreting, so a second belt inside the + // system implies a giant. The converse is not asserted: a giant near the edge has no room for a + // gap inside it, and a cramped neighbourhood may have no free cell to put one in. + int minSpacing = 64; + ClusteredGalaxyGenerator g = gen(minSpacing); + int systemsWithInnerBelt = 0; + for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { + List bodies = g.bodiesFor(SEED, anchor); + boolean hasGiant = false; + int belts = 0; + for (SystemBody b : bodies) { + if (b.kind() == SystemBodyKind.GAS_GIANT) { + hasGiant = true; + } else if (b.kind() == SystemBodyKind.ASTEROID_BELT) { + belts++; + } + } + if (belts > 1) { + systemsWithInnerBelt++; + assertTrue("system " + anchor.cellKey() + " has an inner belt with no giant to have " + + "cleared it", hasGiant); + } + } + assertTrue("the sweep must contain systems with giants and inner belts", systemsWithInnerBelt > 0); + } + + // ─── E2: moons ───────────────────────────────────────────────────────────── + + @Test + public void moonsExistAndLiveInsideTheirParentsCell() { + // Without moons the whole outer system is look-only: nothing out there is landable, because the + // bodies big enough to be out there are the ones with no surface. + int minSpacing = 64; + ClusteredGalaxyGenerator g = gen(minSpacing); + int moons = 0; + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { + List bodies = g.bodiesFor(SEED, anchor); + Set majorCells = new HashSet<>(); + for (SystemBody b : bodies) { + if (b.kind() != SystemBodyKind.MOON) { + majorCells.add(b.name().cellKey()); + } + } + for (SystemBody b : bodies) { + if (b.kind() != SystemBodyKind.MOON) { + continue; + } + moons++; + assertTrue("a moon must share a major body's cell — a planet and its moons are ONE " + + "destination", majorCells.contains(b.name().cellKey())); + assertTrue("a moon must be landable", b.kind().canDescend()); + } + checked++; + } + assertTrue(checked > 5); + assertTrue("a sweep of systems must produce moons", moons > 3); + } + + @Test + public void aMoonIsSomewhereElseInsideItsCellThanItsParent() { + // A moon that never moved inside the cell would be at the cell centre, i.e. exactly where the + // planet is — one address, two bodies, and a descent that cannot say which it came for. + int minSpacing = 64; + ClusteredGalaxyGenerator g = gen(minSpacing); + boolean checkedAny = false; + for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { + for (SystemBody b : g.bodiesFor(SEED, anchor)) { + if (b.kind() != SystemBodyKind.MOON) { + continue; + } + checkedAny = true; + assertFalse("a moon must stand off its cell's centre", + b.inCellOffsetAt(0L).isZero() && b.inCellOffsetAt(6000L).isZero()); + assertTrue("a moon must carry the orbit its climate is derived from — its PARENT's " + + "distance from the star", b.orbitalDistance() > 0); + } + } + assertTrue(checkedAny); + } + + // ─── E6: the layout follows the orbits ───────────────────────────────────── + + @Test + public void aSystemsCellLayoutFollowsItsOrbits() { + // The cell radius is derived from the orbit, so a body further from its star is further from the + // anchor cell. If the two ever came apart, the map would show a system laid out differently from + // the one the physics describes. + int minSpacing = 128; + ClusteredGalaxyGenerator g = gen(minSpacing); + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 2)) { + SystemBody inner = null; + SystemBody outer = null; + for (SystemBody b : g.bodiesFor(SEED, anchor)) { + if (b.kind() == SystemBodyKind.STAR || b.kind() == SystemBodyKind.MOON) { + continue; + } + if (inner == null || b.orbitalDistance() < inner.orbitalDistance()) { + inner = b; + } + if (outer == null || b.orbitalDistance() > outer.orbitalDistance()) { + outer = b; + } + } + if (inner == null || outer == null || inner == outer) { + continue; + } + assertTrue("the outermost body must sit further from the anchor cell than the innermost " + + "(inner " + inner.orbitalDistance() + " at " + + cellDistance(anchor, inner) + ", outer " + outer.orbitalDistance() + + " at " + cellDistance(anchor, outer) + ")", + cellDistance(anchor, outer) >= cellDistance(anchor, inner)); + checked++; + } + assertTrue(checked > 3); + } + + // ─── determinism of the whole retinue ────────────────────────────────────── + + @Test + public void theWholeRetinueIsDeterministic() { + int minSpacing = 64; + ClusteredGalaxyGenerator g = gen(minSpacing); + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 2)) { + assertEquals("a system must regenerate identically", g.bodiesFor(SEED, anchor), + g.bodiesFor(SEED, anchor)); + // And a member cell must answer for the whole system, not just for itself. + List viaAnchor = g.bodiesFor(SEED, anchor); + assertEquals(viaAnchor, g.bodiesFor(SEED, viaAnchor.get(viaAnchor.size() - 1).name())); + checked++; + } + assertTrue(checked > 3); + } + + private static long cellDistance(GalacticCoord anchor, SystemBody body) { + long dx = body.name().sectorX() - anchor.sectorX(); + long dy = body.name().sectorY() - anchor.sectorY(); + long dz = body.name().sectorZ() - anchor.sectorZ(); + return dx * dx + dy * dy + dz * dz; + } +} From bbd40ed4c79f8ee39a599b4956cc1598fe4ae66f Mon Sep 17 00:00:00 2001 From: StannisMod Date: Tue, 11 Aug 2026 22:14:13 +0300 Subject: [PATCH 04/42] test: measure far-coordinate playability on a real player - add /artest player far-tp, delivering like a dimension change - walk, stand and collide measured clean from 0 to 24M - pin the reserved-shipyard teleport veto with predicted outcomes - keep the server far-coordinate integrity spike as a guard --- .../command/test/TestProbeCommand.java | 56 +++ .../SpikeFarCoordinatePlayabilityTest.java | 453 ++++++++++++++++++ .../SpikeFarCoordinateIntegrityTest.java | 153 ++++++ 3 files changed, 662 insertions(+) create mode 100644 src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinatePlayabilityTest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/server/SpikeFarCoordinateIntegrityTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 0f785acbe..baf61f4ff 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -15959,6 +15959,62 @@ private void handlePlayer(MinecraftServer server, ICommandSender sender, String[ + ",\"posZ\":" + player.posZ + "}"); return; } + if ("far-tp".equals(sub) && args.length >= 4) { + // /artest player far-tp + // + // Delivers a CONNECTED player to an arbitrary coordinate, including one + // millions of blocks out, without the anti-cheat having any say in it. + // NetHandlerPlayServer's speed check ("moved too quickly!") measures the + // client's next movement packet against the position captured at the top of + // the tick, and it is skipped entirely while invulnerableDimensionChange is + // armed — the flag vanilla itself sets on every dimension change and clears + // when the client acknowledges the teleport, adopting the destination as the + // last good position. Arming the same flag and then calling the same + // setPlayerLocation a dimension transfer calls makes this the production + // delivery minus the change of dimension. + // + // What a caller DOES have to avoid: Valkyrien Skies vetoes any teleport into + // its reserved shipyard region, silently — the command reports success and + // the player does not move. That region is the half-open quadrant + // chunkX >= CHUNK_X_START - MAX_CHUNK_RADIUS && chunkZ >= -MAX_CHUNK_RADIUS + // (see ShipChunkAllocator), so with the shipped constants any destination + // with X >= 5,094,416 and Z >= -25,584 is refused. Compare the reported posX + // with what you asked for rather than trusting "ok":true. + // + // Deliberately does NOT generate terrain: the caller arranges the + // destination (forceload + fill) so that an arrival into thin air is a + // finding, not a silently patched one. + if (player.connection == null) { + send(sender, "{\"error\":\"far-tp needs a connected player (no connection on \"" + + escapeJson(player.getName()) + "\")\"}"); + return; + } + double tx; + double ty; + double tz; + try { + tx = Double.parseDouble(args[1]); + ty = Double.parseDouble(args[2]); + tz = Double.parseDouble(args[3]); + } catch (NumberFormatException e) { + send(sender, "{\"error\":\"usage: /artest player far-tp \"}"); + return; + } + double fromX = player.posX; + player.motionX = 0; + player.motionY = 0; + player.motionZ = 0; + player.fallDistance = 0; + player.invulnerableDimensionChange = true; + player.connection.setPlayerLocation(tx, ty, tz, player.rotationYaw, player.rotationPitch); + server.getPlayerList().serverUpdateMovingPlayer(player); + send(sender, "{\"ok\":true,\"player\":\"" + escapeJson(player.getName()) + "\"" + + ",\"fromX\":" + fromX + + ",\"posX\":" + player.posX + + ",\"posY\":" + player.posY + + ",\"posZ\":" + player.posZ + "}"); + return; + } if ("held-air".equals(sub)) { // Probe the air-buffer NBT on the player's chest-armor slot // (the canonical AR space-suit slot — ItemSpaceChest wraps diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinatePlayabilityTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinatePlayabilityTest.java new file mode 100644 index 000000000..150306cdd --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinatePlayabilityTest.java @@ -0,0 +1,453 @@ +package zmaster587.advancedRocketry.test.client; + +import com.github.stannismod.forge.testing.junit.AbstractClientE2ETest; +import com.google.gson.JsonObject; + +import org.junit.Test; +import org.lwjgl.input.Keyboard; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.junit.Assert.assertTrue; + +/** + * SPIKE — can a player actually LIVE millions of blocks from the origin, or only exist there? + * + *

Chunk generation, block storage and entity doubles were already measured clean out to 28M with + * the subject SPAWNED at the coordinate. None of them says anything about the thing that decides how + * big a body may be drawn: whether a player walks, stands and collides normally out there.

+ * + *

Why the first attempt could not reach 8M, and why the reason was not vanilla

+ * A connected player could not be delivered past ~4M, and vanilla's speed check + * ({@code NetHandlerPlayServer} "moved too quickly!") was blamed. It is not the cause. The physics + * mod installs a cancellable {@code @Inject} at the HEAD of + * {@code NetHandlerPlayServer.setPlayerLocation} that CANCELS any teleport whose destination it + * considers its own reserved "shipyard" region, and that region is the half-open quadrant + * {@code chunkX >= 318401 && chunkZ >= -1599} — i.e. every position with + * X ≥ 5,094,416 and Z ≥ -25,584. Teleports into it are dropped silently: the command reports + * success, the mixin cancels, and the player never moves. That is exactly the reported symptom, and + * it is a mod-imposed wall five million blocks out, not a vanilla precision limit. + * + *

Two consequences drive this class. {@link #whereExactlyDoesADeliveryStopWorking()} pins the + * boundary against numbers PREDICTED from that predicate, so the mechanism is proven rather than + * inferred from "2M worked and 8M did not". And the playability ladder runs at + * {@code Z = }{@value #ARENA_Z}, below the quadrant's Z edge, where the predicate is false at every + * X — so the original question can be answered out to 28M without touching the physics mod.

+ * + *

Acceptance, stated before the run

+ * Every rung is compared against the {@code x=0} rung measured in the same run, in the same arena, + * with the same key held for the same number of ticks: + *
    + *
  1. Walking distance over {@value #WALK_TICKS} ticks of held {@code W} must be within + * ±10% of the origin's, and at least {@value #MIN_WALK_BLOCKS} blocks absolute.
  2. + *
  3. Collision stand-off from the wall walked into must be within + * {@value #STANDOFF_TOLERANCE} blocks of the origin's — the sharpest instrument here, + * being a sub-block quantity resolved from absolute coordinates.
  4. + *
  5. Standing: {@code posY} within {@value #Y_TOLERANCE} of the floor top throughout.
  6. + *
  7. No rubber-band: server and client agree on {@code posX} to within + * {@value #SYNC_TOLERANCE} blocks at rest.
  8. + *
+ * + *

Designed to come back NO. If 28M behaves like the origin on all four, the ±2M bound has + * nothing left holding it up. If it does not, the rung where it stops is the answer.

+ */ +public class SpikeFarCoordinatePlayabilityTest extends AbstractClientE2ETest { + + /** + * Measured indistinguishable from the origin: 2M, 8M, 16M, 16,777,216 = 2²⁴, 20M, 24M — + * so the suspicion that 2²⁴ is the wall is refuted, and the vanilla wiki's first documented + * horizontal symptom (sound positioning) does not touch walking, standing or collision. + * 28M is the only rung that ever failed, and on both sides at once (client displacement 0.0000, + * not just the server's), which rules out the server dragging him back. + * + *

28M is deliberately NOT in the ladder. It is the one coordinate that ever failed, and + * it failed for a reason none of arrangement, run position, server-side revert or 2²⁴ explains — + * a ladder of {@code 0, 28M, 24M, 28M, 0} was run for exactly that and both 28M rungs failed + * while the 24M between them and the trailing origin passed. The finding is recorded where a + * finding belongs; keeping a permanently red rung here would only make this class dead weight in + * every client gate. The ladder below is the range the design actually uses — half-cell 16M, with + * 20M and 24M as margin — so this class now guards "a player lives normally at the coordinates + * our cells use", which is a different and durable claim from the one it was written for.

+ */ + private static final int[] X_LADDER = {0, 8_000_000, 16_000_000, 20_000_000, 24_000_000}; + + /** + * The arena's Z. The physics mod's reserved quadrant starts at {@code chunkZ >= -1599} + * (Z ≥ -25,584); everything here sits well below it, so its teleport veto never fires and the + * only thing under test is the coordinate's own magnitude. + */ + private static final int ARENA_Z = -100_000; + + private static final int OVERWORLD = 0; + /** Well above sea level: 2M and 16M are both ocean, and a delivery into water measures the water. */ + private static final int FLOOR_Y = 140; + private static final int STAND_Y = FLOOR_Y + 1; + + /** The corridor runs +X from the player; the wall's near face is this many blocks ahead. */ + private static final int WALL_OFFSET = 16; + private static final int WALK_TICKS = 40; + private static final int RAM_TICKS = 160; + + private static final double MIN_WALK_BLOCKS = 5.0d; + private static final double WALK_RATIO_TOLERANCE = 0.10d; + private static final double STANDOFF_TOLERANCE = 0.05d; + private static final double Y_TOLERANCE = 0.05d; + private static final double SYNC_TOLERANCE = 0.5d; + private static final double ARRIVAL_TOLERANCE = 1.0d; + /** How many (deliver, settle) rounds a rung gets before it is called undeliverable. */ + private static final int DELIVERY_ATTEMPTS = 4; + + private String exec(String cmd) throws Exception { + return String.join("\n", serverClient().execute(cmd)); + } + + /** + * Pins the delivery wall against numbers predicted from the physics mod's own predicate, so the + * mechanism is proven rather than inferred. {@code isChunkInShipyard(cx, cz)} is + * {@code cx >= CHUNK_X_START - MAX_CHUNK_RADIUS && cz >= CHUNK_Z_START - MAX_CHUNK_RADIUS} + * = {@code cx >= 318401 && cz >= -1599}, so the four cases below are decided before the run: + * one block under the X edge moves, one block over it does not, and the same X moves again once + * Z drops below the quadrant. A miss on ANY of the four falsifies the explanation. + */ + @Test + public void whereExactlyDoesADeliveryStopWorking() throws Exception { + bot().waitForWorld(); + exec("gamerule sendCommandFeedback false"); + exec("gamerule logAdminCommands false"); + + List report = new ArrayList<>(); + List wrong = new ArrayList<>(); + // {x, z, expectedToMove} + double[][] cases = { + {5_094_400.5d, 0.5d, 1d}, // chunkX 318400 — one chunk under the edge + {5_094_416.5d, 0.5d, 0d}, // chunkX 318401 — the first reserved chunk + {28_000_000.5d, 0.5d, 0d}, // deep inside the quadrant + {28_000_000.5d, ARENA_Z + 0.5d, 1d}, // same X, Z below the quadrant's edge + }; + for (double[] c : cases) { + boolean expectMove = c[2] != 0d; + String reply = exec("artest player far-tp " + fmt(c[0]) + " 200 " + fmt(c[1])); + double from = field(reply, "fromX"); + double to = field(reply, "posX"); + boolean moved = Math.abs(to - c[0]) < ARRIVAL_TOLERANCE; + boolean unchanged = Math.abs(to - from) < 1e-6d; + report.add("target=(" + fmt(c[0]) + "," + fmt(c[1]) + ")" + + " chunk=(" + (((long) Math.floor(c[0])) >> 4) + "," + (((long) Math.floor(c[1])) >> 4) + ")" + + " predicted=" + (expectMove ? "MOVES" : "CANCELLED") + + " observed=" + (moved ? "MOVED" : unchanged ? "CANCELLED" : "ELSEWHERE(" + to + ")")); + if (moved != expectMove) { + wrong.add(report.get(report.size() - 1)); + } + // Park him back near the origin so the next case starts from a known place. + exec("artest player far-tp 0.5 200 0.5"); + exec("artest server wait " + OVERWORLD + " 20"); + } + + StringBuilder out = new StringBuilder("[SPIKE far-coordinate delivery boundary]\n"); + for (String line : report) { + out.append(" ").append(line).append('\n'); + } + System.out.println(out); + writeReport("far-coordinate-delivery-boundary.txt", out.toString()); + assertTrue("the reserved-quadrant explanation predicts these four outcomes; it missed:\n" + out, + wrong.isEmpty()); + } + + @Test + public void canAPlayerWalkStandAndCollideFarFromTheOrigin() throws Exception { + bot().waitForWorld(); + exec("gamerule sendCommandFeedback false"); + exec("gamerule logAdminCommands false"); + exec("gamerule doMobSpawning false"); + exec("gamerule doDaylightCycle false"); + exec("gamerule doWeatherCycle false"); + exec("weather clear"); + bot().setRenderDistance(4); + + List report = new ArrayList<>(); + List inconclusive = new ArrayList<>(); + Rung control = null; + + for (int x : X_LADDER) { + buildArena(x); + String arenaFault = inspectArena(x); + if (arenaFault != null) { + buildArena(x); // one retry: a fill can lose a race with chunk loading + arenaFault = inspectArena(x); + } + if (arenaFault != null) { + inconclusive.add("x=" + x + " the arena did not build - " + arenaFault + + " (arrangement, not the coordinate)"); + continue; + } + + String delivery = deliverAndStand(x); + if (delivery != null) { + inconclusive.add("x=" + x + " " + delivery); + continue; + } + + double startServerX = serverX(); + double startClientX = clientX(); + double startY = serverY(); + + bot().setLook(-90f, 0f); // yaw -90 = east = +X, straight down the corridor + bot().waitTicks(5); + + bot().holdKey(Keyboard.KEY_W); + bot().waitTicks(WALK_TICKS); + double walkedServerX = serverX(); + // Read the CLIENT's own displacement beside the server's, at the one moment it can still + // discriminate. A rung where the player barely moves has two completely different causes + // — the client never walked, or it walked and the server dragged it back — and by the + // time everything is at rest they agree either way, so "sync" at rest cannot tell them + // apart. This sample can. + double walkedClientX = clientX(); + double midY = serverY(); + // Keep the key held: the collision is measured with exactly the input that produced the + // distance above. + bot().waitTicks(RAM_TICKS); + bot().releaseKey(Keyboard.KEY_W); + bot().waitTicks(20); + + double finalServerX = serverX(); + double finalClientX = clientX(); + double finalY = serverY(); + + double walked = walkedServerX - startServerX; + // The wall's near face is at x+WALL_OFFSET; the player's box is 0.6 wide, so a clean + // collision leaves his centre 0.3 short of it. + double standoff = (x + WALL_OFFSET) - finalServerX; + + Rung rung = new Rung(x, walked, walkedClientX - startClientX, standoff, startY, midY, + finalY, Math.abs(finalServerX - finalClientX), + Math.abs(startServerX - startClientX)); + if (x == 0 && control == null) { + control = rung; // the FIRST origin rung; a trailing one is judged against it + } + report.add(rung.line(control)); + } + + StringBuilder out = new StringBuilder("[SPIKE far-coordinate playability] walkTicks=" + WALK_TICKS + + " ramTicks=" + RAM_TICKS + " wallOffset=" + WALL_OFFSET + " arenaZ=" + ARENA_Z + "\n"); + for (String line : report) { + out.append(" ").append(line).append('\n'); + } + for (String line : inconclusive) { + out.append(" INCONCLUSIVE ").append(line).append('\n'); + } + System.out.println(out); + writeReport("far-coordinate-playability.txt", out.toString()); + + assertTrue("no rung produced a usable measurement:\n" + out, !report.isEmpty()); + assertTrue("the x=0 control rung must be measurable - without it no far rung means anything:\n" + + out, control != null); + + List verdicts = new ArrayList<>(); + for (String line : report) { + if (line.contains("VERDICT=FAIL")) { + verdicts.add(line); + } + } + assertTrue("a far coordinate did not behave like the origin:\n" + out, verdicts.isEmpty()); + } + + // ─── arrangement ──────────────────────────────────────────────────────────── + + /** + * A sealed stone corridor running +X from {@code x}, with a wall across it at + * {@code x + WALL_OFFSET}, built BEFORE the player is delivered. + */ + private void buildArena(int x) throws Exception { + int fromChunk = (x - 8) >> 4; + int toChunk = (x + WALL_OFFSET + 8) >> 4; + int fromChunkZ = (ARENA_Z - 8) >> 4; + int toChunkZ = (ARENA_Z + 8) >> 4; + for (int cx = fromChunk; cx <= toChunk; cx++) { + for (int cz = fromChunkZ; cz <= toChunkZ; cz++) { + exec("artest chunk forceload " + OVERWORLD + " " + cx + " " + cz); + } + } + exec("artest server wait " + OVERWORLD + " 60"); + + int x1 = x - 4; + int x2 = x + WALL_OFFSET + 4; + exec("artest fill " + OVERWORLD + " " + x1 + " " + FLOOR_Y + " " + (ARENA_Z - 6) + " " + + x2 + " " + (FLOOR_Y + 6) + " " + (ARENA_Z + 6) + " minecraft:stone"); + // Hollow out everything up to (but not including) the wall plane at x+WALL_OFFSET. + exec("artest fill " + OVERWORLD + " " + (x1 + 1) + " " + STAND_Y + " " + (ARENA_Z - 5) + " " + + (x + WALL_OFFSET - 1) + " " + (FLOOR_Y + 5) + " " + (ARENA_Z + 5) + " minecraft:air"); + exec("artest server wait " + OVERWORLD + " 20"); + } + + /** + * Reads the arena back and reports the first thing that is not what it should be. + * + *

The first version of this control only checked that the FLOOR and the WALL are stone, and it + * passed at every rung — including the one where the player then stood motionless through 200 + * ticks of held {@code W}. It could not fail on the thing that actually matters: whether the + * corridor he has to walk down is air. A player delivered into solid stone stands at + * exactly the right Y and cannot move a millimetre, which reads precisely like "movement is + * broken at this coordinate". So the walkable line is now sampled along its whole length.

+ * + * @return {@code null} if the arena is sound, else what was wrong and what was actually read + */ + private String inspectArena(int x) throws Exception { + for (int dx : new int[] {0, 1, 2, 5, 10, WALL_OFFSET - 2}) { + String at = exec("artest block at " + OVERWORLD + " " + (x + dx) + " " + STAND_Y + " " + + ARENA_Z); + if (!at.contains("minecraft:air")) { + return "the corridor is not air at x+" + dx + " (" + oneLine(at) + ")"; + } + } + for (int dx : new int[] {0, 8, WALL_OFFSET - 1}) { + String at = exec("artest block at " + OVERWORLD + " " + (x + dx) + " " + FLOOR_Y + " " + + ARENA_Z); + if (!at.contains("stone")) { + return "the floor is not stone at x+" + dx + " (" + oneLine(at) + ")"; + } + } + String wall = exec("artest block at " + OVERWORLD + " " + (x + WALL_OFFSET) + " " + STAND_Y + + " " + ARENA_Z); + if (!wall.contains("stone")) { + return "the wall is not stone (" + oneLine(wall) + ")"; + } + return null; + } + + /** + * Delivers the player into the arena and does not return until he is STANDING in it. + * + *

One delivery is not enough and the first run proved it: the chunks are force-loaded on the + * server but the CLIENT has not received them yet, so client-side physics see air, he falls + * through the floor, and the server accepts his movement packets. Delivering again once the + * chunks have arrived is what makes him stay. The loop converges rather than guessing a settle + * time, and reports which of the two conditions it never met.

+ * + * @return {@code null} once he is standing, or a reason string for the INCONCLUSIVE list + */ + private String deliverAndStand(int x) throws Exception { + double lastX = Double.NaN; + double lastY = Double.NaN; + String lastReply = ""; + for (int attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt++) { + lastReply = exec("artest player far-tp " + fmt(x + 0.5d) + " " + STAND_Y + " " + + fmt(ARENA_Z + 0.5d)); + exec("artest server wait " + OVERWORLD + " 40"); + bot().waitTicks(30); + lastX = serverX(); + lastY = serverY(); + if (Math.abs(lastX - (x + 0.5d)) < ARRIVAL_TOLERANCE + && Math.abs(lastY - STAND_Y) < Y_TOLERANCE) { + return null; + } + } + boolean arrived = Math.abs(lastX - (x + 0.5d)) < ARRIVAL_TOLERANCE; + return (arrived + ? "he arrived but would not stand (posY=" + fmt(lastY) + ", floor top " + STAND_Y + + ") - he is falling through a floor the client has not received" + : "the player never arrived (server posX=" + lastX + ", wanted " + (x + 0.5d) + ")") + + " after " + DELIVERY_ATTEMPTS + " deliveries - arrangement, not the coordinate." + + " lastReply=" + oneLine(lastReply); + } + + // ─── instruments ──────────────────────────────────────────────────────────── + + private double serverX() throws Exception { + return field(exec("artest player health"), "posX"); + } + + private double serverY() throws Exception { + return field(exec("artest player health"), "posY"); + } + + private double clientX() throws Exception { + JsonObject state = bot().reportState(); + return state.has("playerX") ? state.get("playerX").getAsDouble() : Double.NaN; + } + + private static double field(String json, String key) { + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("\"" + key + "\"\\s*:\\s*([-0-9.eE]+)").matcher(json); + return m.find() ? Double.parseDouble(m.group(1)) : Double.NaN; + } + + /** One rung's four numbers plus the verdict they earn against the origin control. */ + private static final class Rung { + final int x; + final double walked; + final double clientWalked; + final double standoff; + final double startY; + final double midY; + final double finalY; + final double syncAtRest; + final double syncAtStart; + + Rung(int x, double walked, double clientWalked, double standoff, double startY, double midY, + double finalY, double syncAtRest, double syncAtStart) { + this.x = x; + this.walked = walked; + this.clientWalked = clientWalked; + this.standoff = standoff; + this.startY = startY; + this.midY = midY; + this.finalY = finalY; + this.syncAtRest = syncAtRest; + this.syncAtStart = syncAtStart; + } + + String line(Rung control) { + List failures = new ArrayList<>(); + if (walked < MIN_WALK_BLOCKS) { + failures.add("walked<" + MIN_WALK_BLOCKS); + } + if (Math.abs(startY - STAND_Y) > Y_TOLERANCE + || Math.abs(midY - STAND_Y) > Y_TOLERANCE + || Math.abs(finalY - STAND_Y) > Y_TOLERANCE) { + failures.add("leftTheFloor"); + } + if (syncAtRest > SYNC_TOLERANCE) { + failures.add("serverClientDisagree"); + } + if (control != null && control != this) { + double ratio = control.walked == 0 ? Double.NaN : walked / control.walked; + if (!(Math.abs(ratio - 1d) <= WALK_RATIO_TOLERANCE)) { + failures.add("walkRatio=" + fmt(ratio)); + } + if (!(Math.abs(standoff - control.standoff) <= STANDOFF_TOLERANCE)) { + failures.add("standoffDelta=" + fmt(standoff - control.standoff)); + } + } + return "x=" + x + + " walked=" + fmt(walked) + "(client " + fmt(clientWalked) + ")" + + " standoff=" + fmt(standoff) + + " y=" + fmt(startY) + "/" + fmt(midY) + "/" + fmt(finalY) + + " sync=" + fmt(syncAtStart) + "->" + fmt(syncAtRest) + + " VERDICT=" + (failures.isEmpty() ? "OK" : "FAIL" + failures); + } + } + + private static void writeReport(String name, String text) { + try { + Path dir = Paths.get("build", "spike-reports").toAbsolutePath(); + Files.createDirectories(dir); + Files.write(dir.resolve(name), text.getBytes("UTF-8")); + } catch (Exception e) { + System.out.println("[SPIKE] could not write the report file: " + e); + } + } + + private static String oneLine(String s) { + return s.replace((char) 10, ' ').replace((char) 13, ' ').trim(); + } + + private static String fmt(double v) { + return String.format(Locale.ROOT, "%.4f", v); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/SpikeFarCoordinateIntegrityTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/SpikeFarCoordinateIntegrityTest.java new file mode 100644 index 000000000..d1e548df6 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/server/SpikeFarCoordinateIntegrityTest.java @@ -0,0 +1,153 @@ +package zmaster587.advancedRocketry.test.server; + +import com.github.stannismod.forge.testing.junit.AbstractHeadlessServerTest; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertTrue; + +/** + * SPIKE — does anything actually break past ±2M blocks, the bound the space cell was sized for? + * + *

`space-model.md` says a 4M cell exists because "entity doubles / chunks / lighting degrade past + * ~±2M blocks in 1.12.2". Three mechanisms in one sentence, and the cell size — hence how big a planet + * may be drawn — rests on all three. This measures the two a server can see: chunk generation + * and block storage. The third (render jitter) is client-side and is measured separately.

+ * + *

This spike is designed to come back NO. The acceptance number is stated here, before the + * run: at each sampled X, terrain must generate (a non-air top block at a plausible height) and a + * placed block must read back as itself. A coordinate where either fails is a real ceiling; a + * coordinate where both hold tells us the ceiling is not here.

+ * + *

Throwaway by intent: it exists to answer one question once. If it is kept, it becomes a + * regression test for "the world still works at the coordinates our cells use", which is a different + * claim from the one it was written for.

+ */ +public class SpikeFarCoordinateIntegrityTest extends AbstractHeadlessServerTest { + + /** + * The ladder. 2M is today's half-cell; 8M and 16M are the growth steps a bigger cell would need; + * 28M is just inside the vanilla world border (29 999 984), i.e. the last coordinate that can + * exist at all. + */ + private static final int[] X_LADDER = {2_000_000, 8_000_000, 16_000_000, 28_000_000}; + + private static final int OVERWORLD = 0; + private static final int PLACE_Y = 100; + + private String exec(String cmd) throws Exception { + return String.join("\n", client().execute(cmd)); + } + + @Test + public void chunksAndBlockStorageStillWorkFarFromTheOrigin() throws Exception { + List report = new ArrayList<>(); + List broken = new ArrayList<>(); + + for (int x : X_LADDER) { + int chunkX = x >> 4; + exec("artest chunk forceload " + OVERWORLD + " " + chunkX + " 0"); + // Generation at a fresh, distant chunk is not instant; give the server real ticks rather + // than reading an empty chunk and calling it a ceiling. + exec("artest server wait " + OVERWORLD + " 40"); + + String sample = exec("artest worldgen sample " + OVERWORLD + " " + chunkX + " 0"); + String placed = exec("artest place " + OVERWORLD + " " + x + " " + PLACE_Y + " 0 " + + "minecraft:diamond_block"); + String readBack = exec("artest block at " + OVERWORLD + " " + x + " " + PLACE_Y + " 0"); + + boolean terrainOk = !sample.contains("\"error\"") && !sample.contains("minecraft:air"); + boolean storageOk = readBack.contains("diamond_block"); + report.add("x=" + x + " terrain=" + (terrainOk ? "ok" : "FAIL") + " storage=" + + (storageOk ? "ok" : "FAIL") + " sample=" + oneLine(sample) + + " placed=" + oneLine(placed) + " readBack=" + oneLine(readBack)); + if (!terrainOk || !storageOk) { + broken.add(Integer.toString(x)); + } + } + + // The whole point is the REPORT, so it is emitted either way — a spike that only speaks when + // it fails cannot tell you where the ceiling ISN'T. + System.out.println("[SPIKE far-coordinate integrity]"); + for (String line : report) { + System.out.println(" " + line); + } + + assertTrue("terrain or block storage failed at: " + broken + "\n" + String.join("\n", report), + broken.isEmpty()); + } + + /** + * Do ENTITY DOUBLES hold a sub-block X far from the origin? + * + *

This is the third of `space-model.md`'s three claimed mechanisms, and the only delivery that + * can reach the far coordinates at all: the subject is SPAWNED there rather than moved there. + * Every earlier attempt teleported a connected player and was rubber-banded by + * {@code NetHandlerPlayServer} ("moved too quickly!"), so it measured the anti-cheat and not the + * coordinate.

+ * + *

Two stands 0.05 apart are spawned at each coordinate. If both read back exactly, entity + * doubles do not degrade there — which is what the arithmetic says they should not: a double's + * ULP at 2.8e7 is about 5e-9 of a block.

+ */ + @Test + public void doEntityDoublesHoldASubBlockXFarFromTheOrigin() throws Exception { + List report = new ArrayList<>(); + List broken = new ArrayList<>(); + for (int x : X_LADDER) { + exec("artest chunk forceload " + OVERWORLD + " " + (x >> 4) + " 0"); + exec("artest server wait " + OVERWORLD + " 40"); + + String near = spawnAndRead(x + 0.5500d); + String far = spawnAndRead(x + 0.6000d); + // Compare NUMERICALLY. Java prints a double above 1e7 in E-notation, so a textual check + // reported 16000000.55 as a miss when the value was exact — the check was wrong, not the + // coordinate, and a spike whose verdict is its own formatting is worse than no spike. + boolean nearOk = Math.abs(asDouble(near) - (x + 0.55d)) < 1e-6d; + boolean farOk = Math.abs(asDouble(far) - (x + 0.60d)) < 1e-6d; + boolean distinct = !near.equals(far); + report.add("x=" + x + " asked " + x + ".55 got " + near + + " | asked " + x + ".60 got " + far + + " | exact=" + (nearOk && farOk) + " distinct=" + distinct); + if (!nearOk || !farOk || !distinct) { + broken.add(Integer.toString(x)); + } + } + System.out.println("[SPIKE far-coordinate entity doubles]"); + for (String line : report) { + System.out.println(" " + line); + } + assertTrue("a sub-block X was lost at: " + broken + " " + String.join(" | ", report), + broken.isEmpty()); + } + + private static double asDouble(String s) { + try { + return Double.parseDouble(s); + } catch (RuntimeException e) { + return Double.NaN; + } + } + + /** Spawn an armour stand at an exact X and return the position the server reports for it. */ + private String spawnAndRead(double x) throws Exception { + String spawned = exec("artest vs drop-stand " + OVERWORLD + " " + + String.format(java.util.Locale.ROOT, "%.4f", x) + " 150 0.5"); + java.util.regex.Matcher idm = java.util.regex.Pattern + .compile("\"entityId\"\\s*:\\s*(-?\\d+)").matcher(spawned); + if (!idm.find()) { + return "NO-SPAWN:" + oneLine(spawned); + } + String info = exec("artest entity info " + OVERWORLD + " " + idm.group(1)); + java.util.regex.Matcher xm = java.util.regex.Pattern + .compile("\"posX\"\\s*:\\s*([-0-9.eE]+)").matcher(info); + return xm.find() ? xm.group(1) : ("UNREADABLE:" + oneLine(info)); + } + + private static String oneLine(String s) { + return s.replace('\n', ' ').replace('\r', ' ').trim(); + } +} From f4a38351894481a3c07aeffceafdae0a447f5ec2 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 12 Aug 2026 08:39:27 +0300 Subject: [PATCH 05/42] fix: a moon's period follows mass, and every star lights the world - derive moon periods from the parent's mass, not its surface gravity - a Jupiter's moons ran 11.2x too slowly on both placement paths - sum stellar flux over the primary and its companions - a black hole's companion no longer cancels its accretion dimming - pin both as contract tests; single-star numbers are unchanged --- .../render/planet/RenderAsteroidSky.java | 2 +- .../render/planet/RenderPlanetarySky.java | 2 +- .../dimension/DimensionProperties.java | 16 ++- .../universe/ClusteredGalaxyGenerator.java | 10 +- .../universe/SystemContent.java | 2 +- .../util/AstronomicalBodyHelper.java | 102 +++++++++++------- .../test/integration/SystemContentTest.java | 58 ++++++++++ .../test/unit/AstronomicalBodyHelperTest.java | 44 +++++++- 8 files changed, 193 insertions(+), 43 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java index ba7c44ef2..8d062ea8a 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java @@ -553,7 +553,7 @@ public void render(float partialTicks, WorldClient world, Minecraft mc) { GL11.glPushMatrix(); float planetPositionTheta = AstronomicalBodyHelper.getParentPlanetThetaFromMoon( - properties.rotationalPeriod, properties.orbitalDist, parentProperties.gravitationalMultiplier, + properties.rotationalPeriod, properties.orbitalDist, (float) parentProperties.getOrbitalMass(), myTheta, properties.baseOrbitTheta); GL11.glRotatef((float) myPhi, 0f, 0f, 1f); diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java index 62864c48f..47d28b170 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java @@ -865,7 +865,7 @@ public void render(float partialTicks, WorldClient world, Minecraft mc) { //Do a whole lotta math to figure out where the parent planet is supposed to be //That 0.3054325f is there because we need to do adjustments for some ^$%^$% reason and it's consistently off by 17.5 degrees - float planetPositionTheta = AstronomicalBodyHelper.getParentPlanetThetaFromMoon(properties.rotationalPeriod, properties.orbitalDist, parentProperties.gravitationalMultiplier, myTheta, properties.baseOrbitTheta); + float planetPositionTheta = AstronomicalBodyHelper.getParentPlanetThetaFromMoon(properties.rotationalPeriod, properties.orbitalDist, (float) parentProperties.getOrbitalMass(), myTheta, properties.baseOrbitTheta); GL11.glRotatef((float) myPhi, 0f, 0f, 1f); GL11.glRotatef(planetPositionTheta, 1f, 0f, 0f); diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java index 0547fd8f2..4a5f87208 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java @@ -572,6 +572,20 @@ public boolean hasBulkProperties() { return mass > BULK_UNSET && radius > BULK_UNSET; } + /** + * The mass, in Earth masses, to use in a two-body orbital law about this body — what a moon's + * period is derived from. + * + *

Falls back to surface gravity when nothing has stated a mass, and that is not a fudge: + * {@code g = M/R²}, so gravity and mass are the same number at one Earth radius, and a body with + * no stated bulk is precisely a body nobody has given a radius. What it replaces IS the fudge — + * every caller used to pass gravity unconditionally, which is exact for Earth and off by + * {@code sqrt(M/g)} for everything else.

+ */ + public double getOrbitalMass() { + return mass > BULK_UNSET ? mass : gravitationalMultiplier; + } + /** * State this body's mass and radius, deriving surface gravity from them unless a gravity was * explicitly authored. @@ -2440,7 +2454,7 @@ public double orbitThetaAt(long worldTick) { double theta = 0d; if (isMoon() && getParentProperties() != null) { theta = AstronomicalBodyHelper.getMoonOrbitalThetaAt(orbitalDist, - getParentProperties().gravitationalMultiplier, worldTick); + (float) getParentProperties().getOrbitalMass(), worldTick); } else { StellarBody host = getStar(); if (host != null) { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 2631cc968..c93eb768d 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -332,13 +332,19 @@ private void addMoons(List bodies, long seed, GalacticCoord anchor, if (moons > max) { moons = max; } - double parentGravity = Math.max(0.05d, parentProfile.gravityPercent() / 100d); + // A moon's period comes from its parent's MASS. Passing gravity here is exact only at one + // Earth radius and made a giant's moons crawl — Jupiter is 318 Earth masses but 2.53 g, a + // factor of sqrt(318/2.53) = 11.2 in the period. A profile with no mass falls back to gravity, + // which is the same number for the one-Earth-radius body an unstated bulk describes. + double parentMass = parentProfile.massEarths() > 0d + ? parentProfile.massEarths() + : Math.max(0.05d, parentProfile.gravityPercent() / 100d); for (int j = 1; j <= moons; j++) { int moonOrbit = MOON_MIN_ORBIT + (int) (CellHash.norm( CellHash.ofBody(seed, parent, j, SALT_MOONRAD)) * MOON_ORBIT_SPAN); double theta = CellHash.norm(CellHash.ofBody(seed, parent, j, SALT_MOONANG)) * 2d * Math.PI; double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY - * AstronomicalBodyHelper.getMoonOrbitalPeriod(moonOrbit, (float) parentGravity); + * AstronomicalBodyHelper.getMoonOrbitalPeriod(moonOrbit, (float) parentMass); BodyEphemeris law = BodyEphemeris.orbit(moonOrbit, theta, 0d, false, periodTicks, SystemContent.MOON_UNIT_BLOCKS); bodies.add(new SystemBody(parent, CellFrame.staticAt(parent), law, SystemBodyKind.MOON, diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java index 074e7a822..7736cb920 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java @@ -191,7 +191,7 @@ private static BodyEphemeris orbitLawOf(DimensionProperties planet, StellarBody /** A moon's orbital law about its PARENT — its offset inside the shared cell, live at every tick. */ private static BodyEphemeris moonLawOf(DimensionProperties moon, DimensionProperties parent) { double periodTicks = TICKS_PER_DAY * AstronomicalBodyHelper.getMoonOrbitalPeriod( - moon.getOrbitalDist(), parent.gravitationalMultiplier); + moon.getOrbitalDist(), (float) parent.getOrbitalMass()); return BodyEphemeris.orbit(moon.getOrbitalDist(), moon.baseOrbitTheta, moon.orbitalPhi, moon.isRetrograde, periodTicks, MOON_UNIT_BLOCKS); } diff --git a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java index a572af5a6..b1647d8f3 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java +++ b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java @@ -68,13 +68,20 @@ public static double getOrbitalPeriod(int orbitalDistance, float solarSize) { /** * Returns the orbital period for a body at a given distance around its parent planet * + *

The second argument is a MASS, in Earth masses, and callers used to pass surface gravity. + * The two agree only at one Earth radius — {@code g = M/R²} — so the substitution was exact for + * Earth and wrong by {@code sqrt(M/g)} everywhere else, which for Jupiter (M=318, g=2.53) made its + * moons orbit 11.2 times too slowly. Pass the body's mass; where nothing has stated one, its + * gravity IS the right stand-in, because a body with no stated bulk is a body assumed to be one + * Earth radius across.

+ * * @param orbitalDistance the distance from the parent body - * @param planetaryMass the mass of the planet in question + * @param planetaryMass the mass of the planet in question, in Earth masses * @return the orbital period in MC Days (24000 ticks) */ public static double getMoonOrbitalPeriod(float orbitalDistance, float planetaryMass) { //One (lunar) MC month is 8 MC days, so the moon orbits in 8 - //The same a the function for planets, but since gravity is directly correlated with mass uses the gravity of the plant for mass + //The same as the function for planets, with the parent's mass in place of the star's size return DAYS_PER_LUNAR_MONTH * Math.pow(Math.pow((orbitalDistance / (double) DISTANCE_UNITS_PER_AU), 3) / planetaryMass, 0.5d); } @@ -111,12 +118,12 @@ public static double getOrbitalThetaAt(int orbitalDistance, float solarSize, lon /** * Returns the orbital theta for a body at a given distance around its parent planet, at this current moment * - * @param orbitalDistance the distance from the parent body - * @param parentGravitationalMultiplier the size of the parent planet in question + * @param orbitalDistance the distance from the parent body + * @param parentMassEarths the mass of the parent planet, in Earth masses * @return the current angle around the planet in radians */ - public static double getMoonOrbitalTheta(int orbitalDistance, float parentGravitationalMultiplier) { - return getMoonOrbitalThetaAt(orbitalDistance, parentGravitationalMultiplier, + public static double getMoonOrbitalTheta(int orbitalDistance, float parentMassEarths) { + return getMoonOrbitalThetaAt(orbitalDistance, parentMassEarths, AdvancedRocketry.proxy.getWorldTimeUniversal(0)); } @@ -126,11 +133,11 @@ public static double getMoonOrbitalTheta(int orbitalDistance, float parentGravit * * @return the angle around the parent planet in RADIANS */ - public static double getMoonOrbitalThetaAt(int orbitalDistance, float parentGravitationalMultiplier, + public static double getMoonOrbitalThetaAt(int orbitalDistance, float parentMassEarths, long worldTick) { //Because the function is still in AU and solar mass, some correctional factors to convert to those units double periodTicks = (double) TICKS_PER_DAY - * getMoonOrbitalPeriod(orbitalDistance, parentGravitationalMultiplier); + * getMoonOrbitalPeriod(orbitalDistance, parentMassEarths); if (!(periodTicks > 0d) || Double.isInfinite(periodTicks)) { return 0d; } @@ -140,19 +147,19 @@ public static double getMoonOrbitalThetaAt(int orbitalDistance, float parentGrav /** * Returns the visual orbital theta for a body at a given distance around its parent planet, at this current moment, as a value from 0 - 360 * - * @param rotationalPeriod the rotational period of the moon we are rendering from - * @param orbitalDistance the distance from the parent body - * @param parentGravitationalMultiplier the distance from the parent body - * @param currentOrbitalTheta the orbital theta of the moon we are rendering from - * @param baseOrbitalTheta the base orbital theta of the planet in question + * @param rotationalPeriod the rotational period of the moon we are rendering from + * @param orbitalDistance the distance from the parent body + * @param parentMassEarths the mass of the parent planet, in Earth masses + * @param currentOrbitalTheta the orbital theta of the moon we are rendering from + * @param baseOrbitalTheta the base orbital theta of the planet in question * @return the current angle around the planet normalized 0 - 360, for GL calls */ - public static float getParentPlanetThetaFromMoon(int rotationalPeriod, int orbitalDistance, float parentGravitationalMultiplier, double currentOrbitalTheta, double baseOrbitalTheta) { + public static float getParentPlanetThetaFromMoon(int rotationalPeriod, int orbitalDistance, float parentMassEarths, double currentOrbitalTheta, double baseOrbitalTheta) { //Convert from radians to degrees for easier math float degreeOrbitalTheta = (float) (currentOrbitalTheta * 180 / Math.PI); //Computer the number of rotations per revolution and use that for how fast the planet would seem to orbit from the moon //Planet will not move at all if it is tidally locked - float planetPositionTheta = (((float) (AstronomicalBodyHelper.getMoonOrbitalPeriod(orbitalDistance, parentGravitationalMultiplier) * TICKS_PER_DAY) / rotationalPeriod) - 1) * degreeOrbitalTheta; + float planetPositionTheta = (((float) (AstronomicalBodyHelper.getMoonOrbitalPeriod(orbitalDistance, parentMassEarths) * TICKS_PER_DAY) / rotationalPeriod) - 1) * degreeOrbitalTheta; //Add the base orbital theta so the planet is in the correct place return (planetPositionTheta + (float) (baseOrbitalTheta * 180 / Math.PI)) % 360; } @@ -191,31 +198,34 @@ public static double getStellarBrightness(StellarBody star, int orbitalDistance) if (star == null || orbitalDistance <= 0) { return MIN_BRIGHTNESS; } - //Normal stars are 1.0 times this value, black holes with accretion discs emit less and so modify it - float lightMultiplier = 1.0f; - //Make all values ratios of Earth normal to get ratio compared to Earth - float normalizedStarTemperature = star.getTemperature() / (float) TEMPERATURE_UNITS_PER_SOL; float planetaryOrbitalRadius = orbitalDistance / (float) DISTANCE_UNITS_PER_AU; - //Check to see if the star is a black hole - boolean blackHole = star.isBlackHole(); - Iterable subs = star.getSubStars(); - if (subs != null) { - for (StellarBody star2 : subs) { - if (star2 != null && !star2.isBlackHole()) { - blackHole = false; - break; + // EVERY star that shines on this world contributes, and what ADDS is the FLUX each one + // delivers here — not their luminosities. Radiant power from mutually incoherent sources + // superposes linearly, so E = sum of L_i / d_i², with each star's own distance under its own + // luminosity. Summing luminosities first and dividing once is the same number only while all + // the stars are equidistant from the planet. + // + // Today they are, by construction rather than by physics: a companion's separation is stored + // as an ANGLE in the sky (StellarBody.getStarSeparation), so there is no distance to give it, + // and every companion is fed the primary's. That is exact for the close binaries the model can + // actually describe, and it is why this sums flux terms rather than luminosities — when a + // companion gains a real orbital radius, only the argument below changes. + // + // This replaces a walk over the companions whose only effect was to clear a boolean: any + // ordinary companion turned the accretion-disc dimming OFF, after which the brightness came + // from the BLACK HOLE's own size and temperature at full strength, and the companion itself + // never contributed a photon. + //Returns ratio compared to a planet at 1 AU for Sol, because the other values in AR are normalized, + //and this works fairly well for hooking into with other mod's solar panels & such + double brightness = fluxOf(star, planetaryOrbitalRadius); + Iterable companions = star.getSubStars(); + if (companions != null) { + for (StellarBody companion : companions) { + if (companion != null) { + brightness += fluxOf(companion, planetaryOrbitalRadius); } } } - //There's no real easy way to get the light emitted by an accretion disc, so this substitutes - if (blackHole) - lightMultiplier *= 0.25; - //Returns ratio compared to a planet at 1 AU for Sol, because the other values in AR are normalized, - //and this works fairly well for hooking into with other mod's solar panels & such - double brightness = - lightMultiplier * - ((Math.pow(star.getSize(), 2) * Math.pow(normalizedStarTemperature, 4)) / - Math.pow(planetaryOrbitalRadius, 2)); // Guarantee: never return 0, NaN, or Infinity if (!Double.isFinite(brightness) || brightness < MIN_BRIGHTNESS) { @@ -224,6 +234,26 @@ public static double getStellarBrightness(StellarBody star, int orbitalDistance) return brightness; } + /** + * The flux one star delivers at {@code orbitalRadiusAu}, relative to Sol at 1 AU: + * {@code size² · (T/Sol)⁴ / r²} — Stefan-Boltzmann over the inverse square, both in solar units. + * Quartered for a black hole, because there is no easy way to model what an accretion disc emits. + * + *

0.25 is a power of two, so applying it to the numerator rather than to the finished quotient + * is exact: a system of one star returns bit-identical numbers to the version that multiplied at + * the end.

+ */ + private static double fluxOf(StellarBody star, float orbitalRadiusAu) { + //Make all values ratios of Earth normal to get ratio compared to Earth + float normalizedStarTemperature = star.getTemperature() / (float) TEMPERATURE_UNITS_PER_SOL; + double luminosity = Math.pow(star.getSize(), 2) * Math.pow(normalizedStarTemperature, 4); + //There's no real easy way to get the light emitted by an accretion disc, so this substitutes + if (star.isBlackHole()) { + luminosity *= 0.25d; + } + return luminosity / Math.pow(orbitalRadiusAu, 2); + } + /** * Returns the human-eye-perceivable brightness of this insolation multiplier * diff --git a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java index c012eec9b..d39acbcd3 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java @@ -10,6 +10,7 @@ import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; import zmaster587.advancedRocketry.dimension.DimensionManager; import zmaster587.advancedRocketry.dimension.DimensionProperties; +import zmaster587.advancedRocketry.space.BlockDelta; import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.test.MinecraftBootstrap; import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; @@ -385,6 +386,63 @@ public void aMoonsOffsetInsideItsParentsCellIsLiveWhileItsNameIsNot() { planetBody.inCellOffsetAt(quarterPeriod).isZero()); } + /** + * A moon's period is set by its parent's MASS, not by the gravity you would feel standing on it. + * + *

The two are the same number only at one Earth radius — {@code g = M/R²} — and every orbital + * law here used to be handed gravity. Exact for Earth; for a Jupiter (318 Earth masses, 2.53 g) + * wrong by {@code sqrt(318/2.53)}, so a giant's moons crawled round it 11 times too slowly. The + * fixture below is that Jupiter, and the two readings are 11× apart, so a run cannot satisfy this + * test by accident.

+ */ + @Test + public void aMoonsPeriodFollowsItsParentsMassNotItsSurfaceGravity() { + StellarBody star = new StellarBody(); + star.setId(4251); + star.setSize(1f); + DimensionProperties parent = planet(780, 200, 0.5); + parent.setBulk(318d, 11.2d); // a Jupiter: gravity falls out as M/R² = 2.53 + DimensionProperties moon = planet(781, 127, 0.9); + DimensionManager.getInstance().setDimProperties(780, parent); + DimensionManager.getInstance().setDimProperties(781, moon); + parent.setStar(star); + moon.setParentPlanet(parent); + + assertEquals("the fixture must be a giant, or the two readings coincide and prove nothing", + 2.535d, parent.gravitationalMultiplier, 0.01d); + + long massPeriodTicks = (long) (24000d + * AstronomicalBodyHelper.getMoonOrbitalPeriod(127f, (float) parent.getOrbitalMass())); + long gravityPeriodTicks = (long) (24000d + * AstronomicalBodyHelper.getMoonOrbitalPeriod(127f, parent.gravitationalMultiplier)); + assertTrue("mass and gravity must give periods far enough apart to tell apart: " + + massPeriodTicks + " vs " + gravityPeriodTicks, + gravityPeriodTicks > massPeriodTicks * 5); + + SystemBody moonBody = bodyOf(SystemContent.bodiesOf(star, GalacticCoord.ORIGIN), 781); + assertNotNull(moonBody); + + BlockDelta start = moonBody.inCellOffsetAt(0L); + BlockDelta afterOnePeriod = moonBody.inCellOffsetAt(massPeriodTicks); + BlockDelta afterHalf = moonBody.inCellOffsetAt(massPeriodTicks / 2L); + + // The orbit is 127 units at MOON_UNIT_BLOCKS, so its radius is 25 400 blocks: half a turn puts + // the moon ~50 800 blocks from where it started, and one full turn puts it back. + double halfTurn = separation(start, afterHalf); + double fullTurn = separation(start, afterOnePeriod); + assertTrue("half a mass-derived period must carry the moon to the far side (was " + halfTurn + ")", + halfTurn > 40_000d); + assertTrue("one mass-derived period must bring it back (was " + fullTurn + ")", + fullTurn < 500d); + } + + private static double separation(BlockDelta a, BlockDelta b) { + double dx = a.dx() - b.dx(); + double dy = a.dy() - b.dy(); + double dz = a.dz() - b.dz(); + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + /** * A body on the NEGATIVE side of its star belongs to that star's system, exactly like one on the * positive side. diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java index f0207781e..12d66b31a 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java @@ -86,10 +86,52 @@ public void blackHoleStarReducesBrightness() { blackHole.setBlackHole(true); double dimmed = AstronomicalBodyHelper.getStellarBrightness(blackHole, 100); - // Implementation multiplies by 0.25 when the primary (and all sub-stars) are black holes. + // A black hole emits a quarter of what its size and temperature would otherwise give. assertEquals(normal * 0.25, dimmed, 1e-9); } + /** + * Every star in a system lights the worlds in it. Before this was true, the companion list was + * walked only to decide a boolean and no companion ever contributed a photon. + */ + @Test + public void everyStarInASystemContributesItsOwnLight() { + double alone = AstronomicalBodyHelper.getStellarBrightness(sunLikeStar(), 100); + + StellarBody binary = sunLikeStar(); + binary.addSubStar(sunLikeStar()); + + assertEquals("two identical stars light a world twice as brightly as one does", + 2 * alone, AstronomicalBodyHelper.getStellarBrightness(binary, 100), 1e-9); + } + + /** + * A companion does not repeal the primary's nature. + * + *

The case this pins used to invert: any ordinary companion cleared the black-hole flag, after + * which the luminosity was taken from the BLACK HOLE's own size and temperature at FULL strength — + * so a black hole with a companion came out brighter than a bare one and lit by the wrong body, + * while the companion contributed nothing.

+ */ + @Test + public void aCompanionDoesNotTurnABlackHoleBackIntoAStar() { + double sunAlone = AstronomicalBodyHelper.getStellarBrightness(sunLikeStar(), 100); + + StellarBody bareHole = sunLikeStar(); + bareHole.setBlackHole(true); + double holeAlone = AstronomicalBodyHelper.getStellarBrightness(bareHole, 100); + + StellarBody holeWithCompanion = sunLikeStar(); + holeWithCompanion.setBlackHole(true); + holeWithCompanion.addSubStar(sunLikeStar()); + double together = AstronomicalBodyHelper.getStellarBrightness(holeWithCompanion, 100); + + assertEquals("a black hole and its companion each light the world on their own terms", + holeAlone + sunAlone, together, 1e-9); + assertTrue("the hole stays dimmed: the pair is never as bright as two ordinary stars", + together < 2 * sunAlone); + } + @Test public void planetaryLightLevelMultiplierBaselineIsOne() { assertEquals(1.0, AstronomicalBodyHelper.getPlanetaryLightLevelMultiplier(1.0), 1e-9); From 368461e57f88c7d42705611673353aa276ff3294 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 12 Aug 2026 09:21:28 +0300 Subject: [PATCH 06/42] fix: a year follows stellar mass, a day is drawn, albedo comes from type - key orbital periods on the star's mass, deriving M from R when unstated - rewrite equilibrium temperature over the summed flux, unchanged per star - give every planet type an albedo; ice reflects, lava and ocean do not - delete the (1/g)^3 rotation law from both the realizer and the generator - pin each as a contract test, including one gravity cannot satisfy --- .../api/dimension/solar/StellarBody.java | 32 +++++++++ .../dimension/DimensionManager.java | 8 ++- .../dimension/DimensionProperties.java | 26 ++++++- .../universe/BodyProfile.java | 9 ++- .../universe/PlanetDerivation.java | 35 +++++++++- .../universe/PlanetRealizer.java | 13 ++-- .../universe/PlanetTypePreset.java | 20 ++++++ .../universe/PlanetTypes.java | 22 +++--- .../universe/SystemContent.java | 2 +- .../util/AstronomicalBodyHelper.java | 67 ++++++++++++++----- .../test/unit/AstronomicalBodyHelperTest.java | 55 ++++++++++++++- .../test/unit/PlanetDerivationTest.java | 44 ++++++++++++ 12 files changed, 290 insertions(+), 43 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java b/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java index 5a7e4bceb..87c86e24f 100644 --- a/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java +++ b/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java @@ -15,12 +15,18 @@ public class StellarBody { + /** Sentinel for {@link #mass}: nobody has stated one, so it follows from the radius. */ + public static final float MASS_UNSET = 0f; + /** {@code M ≈ R^1.25} — the inverse of the main-sequence {@code R ≈ M^0.8}. Exact for Sol. */ + private static final double MAIN_SEQUENCE_MASS_EXPONENT = 1.25d; + public List subStars; int numPlanets; int discoveredPlanets; float[] color; int id; float size; + private float mass = MASS_UNSET; String name; short posX, posZ; float starSeperation; @@ -80,6 +86,27 @@ public void setSize(float size) { this.size = size; } + /** + * This star's mass in SOLAR MASSES — what an orbital law about it needs. + * + *

Where nothing has stated one it is derived from the radius through the main-sequence relation + * {@code R ≈ M^0.8}, i.e. {@code M ≈ R^1.25}, which is exact for Sol and the honest reading of a + * star described only by its size. Mass and radius are NOT interchangeable anywhere else: Kepler's + * third law is {@code P ∝ a^1.5 / sqrt(M)}, and feeding it a radius made a 2 R☉ star's planets + * orbit 1.83× too fast and a 0.3 R☉ red dwarf's 2.87× too slowly.

+ */ + public float getMass() { + if (mass > MASS_UNSET) { + return mass; + } + return (float) Math.pow(Math.max(0.01f, size), MAIN_SEQUENCE_MASS_EXPONENT); + } + + /** State this star's mass in solar masses; {@link #MASS_UNSET} hands it back to the radius. */ + public void setMass(float solarMasses) { + this.mass = Math.max(MASS_UNSET, solarMasses); + } + public int getPosX() { return posX; } @@ -233,6 +260,9 @@ public void writeToNBT(NBTTagCompound nbt) { nbt.setShort("posX", posX); nbt.setShort("posZ", posZ); nbt.setFloat("size", size); + if (mass > MASS_UNSET) { + nbt.setFloat("mass", mass); + } nbt.setFloat("seperation", starSeperation); nbt.setBoolean("isBlackHole", isBlackHole); nbt.setFloat("diskAngle", diskAngle); @@ -261,6 +291,8 @@ public void readFromNBT(NBTTagCompound nbt) { if (nbt.hasKey("size")) size = nbt.getFloat("size"); + mass = nbt.hasKey("mass") ? nbt.getFloat("mass") : MASS_UNSET; + if (nbt.hasKey("seperation")) starSeperation = nbt.getFloat("seperation"); diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java index 8ffc6471c..602321e14 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java @@ -337,8 +337,12 @@ public DimensionProperties generateRandom(int starId, String name, int baseAtmos properties.ringColor[2] = properties.skyColor[2]; } - properties.rotationalPeriod = (int) (Math.pow((1 / properties.gravitationalMultiplier), 3) - * DimensionProperties.DEFAULT_ROTATIONAL_PERIOD); + // A day is DRAWN, log-uniform between a quarter and four times the default. It used to be + // (1/g)^3 * DEFAULT — a fabricated law that made spin a function of surface gravity, which has + // no bearing on it, so a half-gravity world got a day eight times longer than Earth's. + double spinFactor = 0.25d * Math.pow(16d, random.nextDouble()); + properties.rotationalPeriod = (int) Math.max(1L, Math.round(spinFactor + * DimensionProperties.DEFAULT_ROTATIONAL_PERIOD)); properties.addBiomes(properties.getViableBiomes(true)); properties.initDefaultAttributes(); diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java index 4a5f87208..10ac98fb0 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java @@ -214,6 +214,12 @@ private static float clampFeatureFrequencyMultiplier(float multiplier) { private double mass = BULK_UNSET; /** This body's radius in Earth radii, or {@link #BULK_UNSET}. See {@link #mass}. */ private double radius = BULK_UNSET; + /** + * The fraction of incident light this world's surface reflects, 0..1 — stated by its TYPE and + * used to derive its temperature. Defaults to Earth's, so a world whose type says nothing keeps + * exactly the temperature it had when 0.3 was hard-coded into the formula. + */ + private double albedo = AstronomicalBodyHelper.EARTH_ALBEDO; /** * Whether {@link #gravitationalMultiplier} was STATED rather than derived. The single bit that keeps * "authored planets are unchanged" true: it is set by the XML element, by the public setter and by @@ -549,6 +555,7 @@ public void resetProperties() { laserDrillOres = new ArrayList<>(); mass = BULK_UNSET; radius = BULK_UNSET; + albedo = AstronomicalBodyHelper.EARTH_ALBEDO; gravityAuthored = false; tidallyLocked = false; metallicity = 1d; @@ -568,6 +575,16 @@ public double getRadius() { return radius; } + /** The fraction of incident light this world reflects, 0..1. */ + public double getAlbedo() { + return albedo; + } + + /** State this world's albedo; clamped to 0..1. */ + public void setAlbedo(double a) { + this.albedo = Math.min(Math.max(a, 0d), 1d); + } + public boolean hasBulkProperties() { return mass > BULK_UNSET && radius > BULK_UNSET; } @@ -1853,6 +1870,7 @@ else if (nbt.hasKey("biomes", NBT.TAG_INT_ARRAY)) { // saved before planets had a mass reloads with exactly the gravity it already had. mass = nbt.hasKey("mass") ? nbt.getDouble("mass") : BULK_UNSET; radius = nbt.hasKey("radius") ? nbt.getDouble("radius") : BULK_UNSET; + albedo = nbt.hasKey("albedo") ? nbt.getDouble("albedo") : AstronomicalBodyHelper.EARTH_ALBEDO; gravityAuthored = nbt.getBoolean("gravityAuthored"); tidallyLocked = nbt.getBoolean("tidallyLocked"); metallicity = nbt.hasKey("metallicity") ? nbt.getDouble("metallicity") : 1d; @@ -2245,6 +2263,9 @@ public void writeToNBT(NBTTagCompound nbt) { if (radius > BULK_UNSET) { nbt.setDouble("radius", radius); } + if (albedo != AstronomicalBodyHelper.EARTH_ALBEDO) { + nbt.setDouble("albedo", albedo); + } if (gravityAuthored) { nbt.setBoolean("gravityAuthored", true); } @@ -2347,7 +2368,8 @@ public void writeToNBT(NBTTagCompound nbt) { */ @Override public int getAverageTemp() { - averageTemperature = AstronomicalBodyHelper.getAverageTemperature(this.getStar(), this.getSolarOrbitalDistance(), this.getAtmosphereDensity()); + averageTemperature = AstronomicalBodyHelper.getAverageTemperature(this.getStar(), + this.getSolarOrbitalDistance(), this.getAtmosphereDensity(), this.albedo); /* int temp = averageTemperature; @@ -2458,7 +2480,7 @@ public double orbitThetaAt(long worldTick) { } else { StellarBody host = getStar(); if (host != null) { - theta = AstronomicalBodyHelper.getOrbitalThetaAt(orbitalDist, host.getSize(), worldTick); + theta = AstronomicalBodyHelper.getOrbitalThetaAt(orbitalDist, host.getMass(), worldTick); } } return (theta + baseOrbitTheta) * (isRetrograde ? -1 : 1); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/BodyProfile.java b/src/main/java/zmaster587/advancedRocketry/universe/BodyProfile.java index a731630d3..306d6daa4 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/BodyProfile.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/BodyProfile.java @@ -33,11 +33,12 @@ public final class BodyProfile { private final boolean hasRings; private final double metallicity; private final TerrainOption terrain; + private final int rotationalPeriodTicks; public BodyProfile(SystemBodyKind kind, String typeName, PlanetTypePreset preset, int orbitalDistance, double massEarths, double radiusEarths, int gravityPercent, int pressure, int temperatureKelvin, boolean hasOxygen, boolean tidallyLocked, boolean hasRings, - double metallicity, TerrainOption terrain) { + double metallicity, TerrainOption terrain, int rotationalPeriodTicks) { this.kind = kind; this.typeName = typeName; this.preset = preset; @@ -52,6 +53,7 @@ public BodyProfile(SystemBodyKind kind, String typeName, PlanetTypePreset preset this.hasRings = hasRings; this.metallicity = metallicity; this.terrain = terrain; + this.rotationalPeriodTicks = Math.max(1, rotationalPeriodTicks); } /** What this body is as an addressable object — planet, giant, moon or belt. */ @@ -75,6 +77,11 @@ public int orbitalDistance() { } /** Mass in Earth masses — PRIMARY, not derived from gravity. */ + /** How long this body takes to turn once, in ticks; drawn from the seed, not derived from gravity. */ + public int rotationalPeriodTicks() { + return rotationalPeriodTicks; + } + public double massEarths() { return massEarths; } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java index a0a97654b..cc9e6d245 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java @@ -53,6 +53,14 @@ public final class PlanetDerivation { private static final long SALT_TERRAIN = 0x28L; private static final long SALT_OXYGEN = 0x29L; private static final long SALT_RINGS = 0x2AL; + private static final long SALT_SPIN = 0x2BL; + + /** A rocky world's day, as a multiple of the default, log-uniform between these. */ + private static final double SPIN_ROCKY_MIN = 0.25d; + private static final double SPIN_ROCKY_MAX = 4.0d; + /** Giants spin fast — a real correlation, unlike the gravity law this replaces. */ + private static final double SPIN_GIANT_MIN = 0.20d; + private static final double SPIN_GIANT_MAX = 0.60d; /** * The temperature, in Kelvin, that defines a star's REFERENCE distance — Earth's equilibrium @@ -289,11 +297,36 @@ public static BodyProfile derive(long seed, GalacticCoord anchor, GalacticCoord && CellHash.norm(CellHash.ofBody(seed, key, variant, SALT_RINGS)) < (giant ? RING_CHANCE_GIANT : RING_CHANCE_ROCKY); + int spin = rotationalPeriodOf(seed, key, variant, giant); + SystemBodyKind kind = giant ? SystemBodyKind.GAS_GIANT : (moon ? SystemBodyKind.MOON : SystemBodyKind.PLANET); return new BodyProfile(kind, preset == null ? PlanetTypes.UNCLASSIFIED : preset.name(), preset, orbitalDistance, mass, radius, gravityPercent, pressure, temperature, oxygen, locked, - rings, metallicity, terrain); + rings, metallicity, terrain, spin); + } + + /** + * How long this body takes to turn once, in ticks. + * + *

DRAWN, not derived — and that is the honest answer. A planet's spin comes from how it + * accreted and what has since torqued it; nothing else this derivation knows predicts it. What it + * replaces was worse than a draw: {@code (1/g)^3 * DEFAULT} made the day a function of SURFACE + * GRAVITY, which has no bearing on rotation at all, so a half-gravity world got a day eight times + * longer. A drawn number is honest; a fabricated law that looks derived is not.

+ * + *

Log-uniform across the band, so short and long days are equally likely by ratio rather than + * by difference. Giants spin fast, which IS a real correlation — angular momentum shed to a large + * envelope — so they take a tighter, faster band. Tidal locking overrides this entirely and is + * applied where the body is realized.

+ */ + static int rotationalPeriodOf(long seed, GalacticCoord key, int variant, boolean giant) { + double lo = giant ? SPIN_GIANT_MIN : SPIN_ROCKY_MIN; + double hi = giant ? SPIN_GIANT_MAX : SPIN_ROCKY_MAX; + double u = CellHash.norm(CellHash.ofBody(seed, key, variant, SALT_SPIN)); + double factor = lo * Math.pow(hi / lo, u); + long ticks = Math.round(factor * DimensionProperties.DEFAULT_ROTATIONAL_PERIOD); + return (int) Math.max(1L, Math.min(ticks, Integer.MAX_VALUE)); } // ─── The individual laws ─────────────────────────────────────────────────── diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java index f737e6880..bc3496580 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java @@ -187,6 +187,8 @@ private static DimensionProperties materialize(int dimId, BodyProfile profile, S PlanetTypePreset preset = profile.preset(); if (preset != null) { + // The type states what the surface is made of, so it states how much light it throws back. + props.setAlbedo(preset.albedo()); if (!preset.biomes().isEmpty()) { XMLPlanetLoader.applyBiomeList(props, preset.biomes()); } @@ -232,19 +234,16 @@ private static void applyTerrain(DimensionProperties props, TerrainOption terrai */ private static int rotationalPeriodOf(BodyProfile profile, StellarBody star) { if (profile.tidallyLocked()) { - double days = AstronomicalBodyHelper.getOrbitalPeriod(profile.orbitalDistance(), star.getSize()); + double days = AstronomicalBodyHelper.getOrbitalPeriod(profile.orbitalDistance(), star.getMass()); double ticks = days * AstronomicalBodyHelper.TICKS_PER_DAY; if (!(ticks > 0d) || ticks > Integer.MAX_VALUE) { return Integer.MAX_VALUE; } return (int) ticks; } - double gravity = Math.max(0.05d, profile.gravityPercent() / 100d); - double period = Math.pow(1d / gravity, 3) * DimensionProperties.DEFAULT_ROTATIONAL_PERIOD; - if (!(period > 0d) || period > Integer.MAX_VALUE) { - return DimensionProperties.DEFAULT_ROTATIONAL_PERIOD; - } - return Math.max(1, (int) period); + // Spin is a property of the body, drawn where every other one is derived. It used to be + // computed here from surface GRAVITY, which does not bear on rotation at all. + return profile.rotationalPeriodTicks(); } /** The angle of a body's cell about its system's anchor, in radians. */ diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypePreset.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypePreset.java index b2f01d19c..bd4c62ae7 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypePreset.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypePreset.java @@ -4,6 +4,7 @@ import java.util.Collections; import java.util.List; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; import zmaster587.advancedRocketry.util.OreGenProperties; /** @@ -44,6 +45,7 @@ public final class PlanetTypePreset { private final List terrain; private final String biomes; private final OreGenProperties oreProperties; + private final double albedo; private PlanetTypePreset(Builder b) { this.name = b.name; @@ -64,6 +66,17 @@ private PlanetTypePreset(Builder b) { : Collections.unmodifiableList(new ArrayList<>(b.terrain)); this.biomes = b.biomes == null ? "" : b.biomes.trim(); this.oreProperties = b.oreProperties; + this.albedo = Math.min(Math.max(b.albedo, 0d), 1d); + } + + /** + * The fraction of incident light this kind of world reflects, 0..1 — what its temperature is + * actually derived from, in place of the single hard-coded 0.3 that used to stand for every + * surface. It belongs to the type because the type IS the statement of what the surface is made + * of, and it closes physically: high albedo means a colder world, which is why ice stays ice. + */ + public double albedo() { + return albedo; } /** The type's name — what a scan reports and what a pack overrides by. */ @@ -206,6 +219,7 @@ public static final class Builder { private final List terrain = new ArrayList<>(); private String biomes = ""; private OreGenProperties oreProperties; + private double albedo = AstronomicalBodyHelper.EARTH_ALBEDO; private Builder(String name) { this.name = name == null ? "" : name.trim(); @@ -277,6 +291,12 @@ public Builder ores(OreGenProperties ores) { return this; } + /** 0..1; defaults to Earth's, so a type that says nothing behaves as it did before. */ + public Builder albedo(double a) { + this.albedo = a; + return this; + } + public PlanetTypePreset build() { return new PlanetTypePreset(this); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java index 09c03dfe8..326d9b2bd 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java @@ -201,7 +201,7 @@ public static List stockPresets() { // The commonest body class of all — every airless moon, Mercury. Defined by having no air at // all, which is why its pressure band is the tight one and its temperature band is not: an // airless rock is as plausible baking beside its star as frozen far from it. - l.add(PlanetTypePreset.builder("barren").weight(30) + l.add(PlanetTypePreset.builder("barren").albedo(0.12d).weight(30) .pressure(0, 25).temperature(0, 1500).gravity(1, 90) .biomes("advancedrocketry:moon;30,advancedrocketry:moondark;20") .terrain(TerrainOption.ofNative(0, 1)) @@ -209,7 +209,7 @@ public static List stockPresets() { // Everything past the snow line, thin-aired or thick: Europa and Titan are the same class of // world, and which of the two you get is how much nitrogen the gravity managed to keep. - l.add(PlanetTypePreset.builder("ice").weight(22) + l.add(PlanetTypePreset.builder("ice").albedo(0.60d).weight(22) .pressure(0, 1600).temperature(0, 200).gravity(1, 400) .biomes("advancedrocketry:moondark;10,minecraft:ice_flats;30,minecraft:ice_mountains;20") .terrain(TerrainOption.ofNative(0, 1)) @@ -217,7 +217,7 @@ public static List stockPresets() { // Tight inner orbits, common around M dwarfs. A molten surface under whatever the rock itself // boiled off, which can be a great deal — hence no pressure ceiling. - l.add(PlanetTypePreset.builder("lava").weight(12) + l.add(PlanetTypePreset.builder("lava").albedo(0.10d).weight(12) .pressure(0, 1600).temperature(700, 6000).gravity(5, 400) .biomes("advancedrocketry:volcanic;30,advancedrocketry:volcanicbarren;20," + "advancedrocketry:hotdryrock;10") @@ -226,7 +226,7 @@ public static List stockPresets() { // Venus-like, and likely common in the hot zone: a thick atmosphere doing the warming, which is // why the band is keyed on the PRESSURE floor rather than on where the world orbits. - l.add(PlanetTypePreset.builder("greenhouse").weight(14) + l.add(PlanetTypePreset.builder("greenhouse").albedo(0.75d).weight(14) .pressure(150, 1600).temperature(275, 1000).gravity(20, 400) .biomes("advancedrocketry:hotdryrock;30,advancedrocketry:volcanicbarren;10") .terrain(TerrainOption.ofNative(0, 1)) @@ -234,21 +234,21 @@ public static List stockPresets() { // The commonest planet class in the galaxy, and absent from the Solar System entirely. Defined // by MASS, not by climate: a super-Earth is one whether it is frozen or baked. - l.add(PlanetTypePreset.builder("superearth").weight(16) + l.add(PlanetTypePreset.builder("superearth").albedo(0.30d).weight(16) .pressure(0, 1600).temperature(0, 900).gravity(160, 400) .biomes("advancedrocketry:stormland;30,advancedrocketry:hotdryrock;10") .terrain(TerrainOption.ofNative(0, 1)) .build()); // A common end state of water loss: warm, dry, and holding just enough air to blow it around. - l.add(PlanetTypePreset.builder("desert").weight(16) + l.add(PlanetTypePreset.builder("desert").albedo(0.30d).weight(16) .pressure(0, 200).temperature(200, 700).gravity(10, 200) .biomes("advancedrocketry:hotdryrock;30,minecraft:desert;20,minecraft:mesa;10") .terrain(TerrainOption.ofNative(0, 1)) .build()); // Hypothesised but plausible: no exposed continent worth the name, and a deep global sea. - l.add(PlanetTypePreset.builder("ocean").weight(7).allowsOxygen(true) + l.add(PlanetTypePreset.builder("ocean").albedo(0.10d).weight(7).allowsOxygen(true) .pressure(60, 400).temperature(255, 380).gravity(50, 190) .seaLevel(96) .biomes("advancedrocketry:oceanspires;30,minecraft:deep_ocean;30,minecraft:ocean;20") @@ -257,7 +257,7 @@ public static List stockPresets() { // Life without oxygen — the crystal / stormland / alien-forest biomes, all written and nearly // unused today. Deliberately narrow: a find, not a background. - l.add(PlanetTypePreset.builder("exotic").weight(5) + l.add(PlanetTypePreset.builder("exotic").albedo(0.30d).weight(5) .pressure(40, 1600).temperature(200, 430).gravity(10, 220) .biomes("advancedrocketry:crystalchasms;30,advancedrocketry:stormland;20," + "advancedrocketry:alien_forest;10") @@ -265,7 +265,7 @@ public static List stockPresets() { .build()); // Very rare, and rare on purpose: the conjunction is physics, the oxygen on top is biology. - l.add(PlanetTypePreset.builder("earthlike").weight(3).allowsOxygen(true) + l.add(PlanetTypePreset.builder("earthlike").albedo(0.30d).weight(3).allowsOxygen(true) .pressure(50, 220).temperature(255, 325).gravity(60, 145) .biomes("minecraft:plains;30,minecraft:forest;25,minecraft:extreme_hills;15," + "minecraft:ocean;15,advancedrocketry:marsh;10") @@ -275,13 +275,13 @@ public static List stockPresets() { // ~10-20% of stars. A real destination — fuel skimming and moons — but never a landing. // Its bands are deliberately the widest in the table: a giant is a giant, and nothing else in // this list will ever admit one, so a gap here would leave a whole body class untyped. - l.add(PlanetTypePreset.builder("gasgiant").weight(14).gasGiant(true).tidallyLockable(false) + l.add(PlanetTypePreset.builder("gasgiant").albedo(0.50d).weight(14).gasGiant(true).tidallyLockable(false) .pressure(0, 1600).temperature(0, 1500).gravity(1, 400) .terrain(TerrainOption.ofNative(0, 1)) .build()); // Neptune and Uranus: the same, further out and colder. - l.add(PlanetTypePreset.builder("icegiant").weight(9).gasGiant(true).tidallyLockable(false) + l.add(PlanetTypePreset.builder("icegiant").albedo(0.50d).weight(9).gasGiant(true).tidallyLockable(false) .pressure(0, 1600).temperature(0, 250).gravity(1, 300) .terrain(TerrainOption.ofNative(0, 1)) .build()); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java index 7736cb920..61ebb0eda 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java @@ -183,7 +183,7 @@ public static List bodiesOf(StellarBody star, GalacticCoord systemCo private static BodyEphemeris orbitLawOf(DimensionProperties planet, StellarBody star) { double periodTicks = star == null ? 0d : TICKS_PER_DAY * AstronomicalBodyHelper.getOrbitalPeriod(planet.getOrbitalDist(), - star.getSize()); + star.getMass()); return BodyEphemeris.orbit(planet.getOrbitalDist(), planet.baseOrbitTheta, planet.orbitalPhi, planet.isRetrograde, periodTicks, ORBIT_UNIT_BLOCKS); } diff --git a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java index b1647d8f3..c852e3945 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java +++ b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java @@ -25,6 +25,20 @@ public class AstronomicalBodyHelper { public static final int KELVIN_PER_STAR_TEMPERATURE_UNIT = 58; /** Solar radii in one astronomical unit — carries a star's size into the distance frame. */ public static final int SOLAR_RADII_PER_AU = 215; + /** + * Earth's albedo — the reflectivity a world is assumed to have when its type has not stated one. + * It was hard-coded into the temperature formula with a comment saying it could not easily be + * calculated; a planet's TYPE knows what its surface is made of, so most callers can do better. + */ + public static final double EARTH_ALBEDO = 0.3d; + /** + * The bare (zero-albedo) equilibrium temperature at one AU from Sol, in Kelvin — the anchor the + * flux form scales from. DERIVED from the constants above rather than written as a literal, so it + * cannot drift away from them: {@code T☉ · sqrt(R☉ / 2 AU)}. + */ + private static final double REFERENCE_EQUILIBRIUM_K = + (double) KELVIN_PER_STAR_TEMPERATURE_UNIT * TEMPERATURE_UNITS_PER_SOL + * Math.sqrt(1d / (2d * SOLAR_RADII_PER_AU)); // ─── The calendar ────────────────────────────────────────────────────────── // Inherited from upstream: "One MC Year is 48 MC days (16 IRL Hours), one month is 8 MC Days". @@ -34,7 +48,7 @@ public class AstronomicalBodyHelper { // as a fraction of the year: changing one does NOT change the other. If that relation is ever // meant to hold, encode it deliberately and record the decision here. - /** Days in one year: the orbital period one AU from a size-1 star. */ + /** Days in one year: the orbital period one AU from a mass-1 star. */ public static final int DAYS_PER_YEAR = 48; /** Days in one lunar month: a moon's period at the reference distance from a mass-1 parent. */ public static final int DAYS_PER_LUNAR_MONTH = 8; @@ -55,14 +69,21 @@ public static float getBodySizeMultiplier(float orbitalDistance) { /** * Returns the orbital period for a body at a given distance around its star * + *

The second argument is the star's MASS in solar masses. Kepler's third law is + * {@code P ∝ a^1.5 / sqrt(M)}; callers used to pass the star's RADIUS, giving + * {@code P ∝ a^1.5 / R^1.5}. Sol is exact because its mass and radius are both 1, and everything + * else was wrong by {@code R^1.5/sqrt(M)} — a 2 R☉ star's year came out 1.83× too short and a + * 0.3 R☉ red dwarf's 2.87× too long, and red dwarfs carry most of the close-in habitable worlds. + * {@link StellarBody#getMass()} derives a mass from the radius where none is stated.

+ * * @param orbitalDistance the distance from the parent body - * @param solarSize the size of the sun in question + * @param starMassSolar the mass of the star in question, in solar masses * @return the orbital period in MC Days (24000 ticks) */ - public static double getOrbitalPeriod(int orbitalDistance, float solarSize) { + public static double getOrbitalPeriod(int orbitalDistance, float starMassSolar) { //One MC Year is 48 MC days (16 IRL Hours), one month is 8 MC Days return DAYS_PER_YEAR - * Math.pow(Math.pow((orbitalDistance / ((double) DISTANCE_UNITS_PER_AU * solarSize)), 3), 0.5d); + * Math.pow(Math.pow(orbitalDistance / (double) DISTANCE_UNITS_PER_AU, 3) / starMassSolar, 0.5d); } /** @@ -90,11 +111,11 @@ public static double getMoonOrbitalPeriod(float orbitalDistance, float planetary * Returns the orbital theta for a body at a given distance around its star, at this current moment * * @param orbitalDistance the distance from the parent body - * @param solarSize the size of the sun in question + * @param starMassSolar the mass of the star in question, in solar masses * @return the current angle around the star in radians */ - public static double getOrbitalTheta(int orbitalDistance, float solarSize) { - return getOrbitalThetaAt(orbitalDistance, solarSize, AdvancedRocketry.proxy.getWorldTimeUniversal(0)); + public static double getOrbitalTheta(int orbitalDistance, float starMassSolar) { + return getOrbitalThetaAt(orbitalDistance, starMassSolar, AdvancedRocketry.proxy.getWorldTimeUniversal(0)); } /** @@ -105,10 +126,10 @@ public static double getOrbitalTheta(int orbitalDistance, float solarSize) { * * @return the angle around the star in RADIANS */ - public static double getOrbitalThetaAt(int orbitalDistance, float solarSize, long worldTick) { - double periodTicks = (double) TICKS_PER_DAY * getOrbitalPeriod(orbitalDistance, solarSize); + public static double getOrbitalThetaAt(int orbitalDistance, float starMassSolar, long worldTick) { + double periodTicks = (double) TICKS_PER_DAY * getOrbitalPeriod(orbitalDistance, starMassSolar); if (!(periodTicks > 0d) || Double.isInfinite(periodTicks)) { - // A degenerate orbit (zero distance, or a star with no size recorded) does not move. + // A degenerate orbit (zero distance, or a star with no mass recorded) does not move. // Answering 0 keeps it addressable instead of handing every caller a NaN coordinate. return 0d; } @@ -173,12 +194,26 @@ public static float getParentPlanetThetaFromMoon(int rotationalPeriod, int orbit * @return the temperature of the planet in Kelvin */ public static int getAverageTemperature(StellarBody star, int orbitalDistance, int atmPressure) { - int starSurfaceTemperature = KELVIN_PER_STAR_TEMPERATURE_UNIT * star.getTemperature(); - float starRadius = star.getSize() / (float) SOLAR_RADII_PER_AU; - //Gives output in AU - float planetaryOrbitalRadius = orbitalDistance / (float) DISTANCE_UNITS_PER_AU; - //Albedo is 0.3f hardcoded because of inability to easily calculate - double averageWithoutAtmosphere = starSurfaceTemperature * Math.pow(starRadius / (2 * planetaryOrbitalRadius), 0.5) * Math.pow((1f - 0.3f), 0.25); + return getAverageTemperature(star, orbitalDistance, atmPressure, EARTH_ALBEDO); + } + + /** + * The same, for a world whose ALBEDO is known — which is the one a planet's type states. + * + *

This is the grey body written over the flux that {@link #getStellarBrightness} already + * computes, rather than a second copy of the same arithmetic: {@code T = T₀ · (E·(1−a))^¼}, with + * {@code T₀} the bare equilibrium temperature at 1 AU from Sol. Algebraically identical to the + * per-star form it replaces — expand {@code E} for a single star and the radii and temperatures + * cancel exactly — so no world's temperature moves. What it buys is that {@code E} is a SUM over + * every star in the system, so a binary's worlds are warmed by both without a second code path.

+ * + * @param albedo the fraction of incident light the surface reflects, 0..1 + */ + public static int getAverageTemperature(StellarBody star, int orbitalDistance, int atmPressure, + double albedo) { + double flux = getStellarBrightness(star, orbitalDistance); + double absorbed = flux * (1d - Math.min(Math.max(albedo, 0d), 1d)); + double averageWithoutAtmosphere = REFERENCE_EQUILIBRIUM_K * Math.pow(absorbed, 0.25d); //Slightly kludgey solution that works out mostly for Venus and well for Earth, without being overly complex //Output is in Kelvin return (int) (averageWithoutAtmosphere diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java index 12d66b31a..4e3aceec9 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java @@ -204,12 +204,63 @@ public void planetaryLightMultiplierWithinExpectedBounds() { // search-and-replace can silently corrupt one of them. // ───────────────────────────────────────────────────────────────────────────── + /** + * A world's temperature follows its ALBEDO, which its type states. The formula used to hard-code + * 0.3 for every surface, so an ice world and a lava world at the same distance were the same + * temperature — and the physical direction matters: more reflective means colder, which is what + * keeps ice being ice. + */ + @Test + public void albedoCoolsAWorldAndTheDefaultIsEarths() { + StellarBody star = sunLikeStar(); + int dark = AstronomicalBodyHelper.getAverageTemperature(star, 100, 0, 0.10d); + int earthLike = AstronomicalBodyHelper.getAverageTemperature(star, 100, 0, 0.30d); + int icy = AstronomicalBodyHelper.getAverageTemperature(star, 100, 0, 0.60d); + + assertTrue("a darker surface absorbs more and runs hotter", dark > earthLike); + assertTrue("a more reflective surface runs colder", icy < earthLike); + assertEquals("the albedo-less form must still mean Earth's albedo", + AstronomicalBodyHelper.getAverageTemperature(star, 100, 0), earthLike); + } + @Test public void orbitalPeriodFollowsTheThreeHalvesPowerLawExactly() { // Four times the distance is eight times the period. assertEquals(384.0, AstronomicalBodyHelper.getOrbitalPeriod(400, 1.0f), 1e-9); - // A bigger star pulls the same distance into a shorter year. - assertEquals(31.176914536239792, AstronomicalBodyHelper.getOrbitalPeriod(150, 2.0f), 1e-9); + // A heavier star pulls the same distance into a shorter year, as sqrt(M) — Kepler's third law, + // P = 48 * a^1.5 / sqrt(M) = 48 * 1.5^1.5 / sqrt(2). The second argument is a MASS in solar + // masses; while it was read as a RADIUS this line expected 31.176914536239792, i.e. 1.5^1.5/2^1.5. + assertEquals(62.353829072479584, AstronomicalBodyHelper.getOrbitalPeriod(150, 2.0f), 1e-9); + } + + /** + * A star's year is set by its MASS. A star that states no mass supplies one from its radius through + * the main-sequence relation, which is exact for Sol — and is emphatically not the radius itself. + */ + @Test + public void aYearIsKeyedOnStellarMassAndAStarWithoutOneDerivesItFromItsRadius() { + StellarBody sol = sunLikeStar(); // size 1.0 + assertEquals("Sol's mass and radius are both 1, so nothing can tell them apart here", + 1.0, sol.getMass(), 1e-6); + assertEquals(48.0, AstronomicalBodyHelper.getOrbitalPeriod(100, sol.getMass()), 1e-9); + + StellarBody big = sunLikeStar(); + big.setSize(2.0f); + // R = 2 gives M = 2^1.25 = 2.3784, so the year is 48/sqrt(2.3784) days. The mass is a float, so + // the exact figure below carries that narrowing — deliberately, per this file's header. + assertEquals(2.378414230005442, big.getMass(), 1e-6); + assertEquals(31.124149808586335, AstronomicalBodyHelper.getOrbitalPeriod(100, big.getMass()), 1e-9); + // A star two Sol-radii across is HEAVIER than two solar masses, so keying the year on its mass + // gives a shorter year than substituting the radius would. Any star but Sol separates the two. + assertTrue("a two-radius star masses more than two Suns", big.getMass() > big.getSize()); + assertTrue("so its year is shorter than a radius substitution gives", + AstronomicalBodyHelper.getOrbitalPeriod(100, big.getMass()) + < AstronomicalBodyHelper.getOrbitalPeriod(100, big.getSize())); + + StellarBody stated = sunLikeStar(); + stated.setSize(2.0f); + stated.setMass(4.0f); + assertEquals("a stated mass wins over the derivation", 4.0, stated.getMass(), 1e-6); } @Test diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java index 491a93369..7aa5c4a2f 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java @@ -76,6 +76,50 @@ private static List system(long seed, GalacticCoord anchor, Stellar return out; } + /** + * A world's DAY is drawn, and it is not a function of its gravity. + * + *

The law this replaced was {@code (1/g)^3 * DEFAULT}: spin computed from SURFACE GRAVITY, which + * has no bearing on rotation, so a half-gravity world got a day eight times longer. The pin that + * catches a return to it is two bodies with the SAME gravity and DIFFERENT days — impossible under + * any function of gravity alone, and cheap to find across a spread of seeds.

+ */ + @Test + public void aDayIsDrawnAndIsNotAFunctionOfGravity() { + GalacticCoord anchor = cell(600, 0, 0); + StellarBody s = sol(); + Map spinByGravity = new HashMap<>(); + boolean sameGravityDifferentDay = false; + int seen = 0; + + for (int i = 0; i < 400 && !sameGravityDifferentDay; i++) { + BodyProfile p = PlanetDerivation.derive(SEED + i, anchor, cell(600 + i, 7, 0), 0, s, false, 140); + int spin = p.rotationalPeriodTicks(); + seen++; + assertTrue("a day must stay inside the drawn band: " + spin, + spin >= 24000 / 5 && spin <= 24000 * 5); + Integer earlier = spinByGravity.put(p.gravityPercent(), spin); + if (earlier != null && earlier.intValue() != spin) { + sameGravityDifferentDay = true; + } + } + + assertTrue("the sweep must actually produce bodies", seen > 0); + assertTrue("two worlds of equal gravity must be able to have different days;" + + " if none did in " + seen + " bodies, spin is a function of gravity again", + sameGravityDifferentDay); + } + + /** The same body answers the same day twice — a draw, not a random. */ + @Test + public void aDrawnDayIsStillDeterministic() { + GalacticCoord anchor = cell(610, 0, 0); + StellarBody s = sol(); + BodyProfile a = PlanetDerivation.derive(SEED, anchor, cell(611, 2, 0), 0, s, false, 150); + BodyProfile b = PlanetDerivation.derive(SEED, anchor, cell(611, 2, 0), 0, s, false, 150); + assertEquals(a.rotationalPeriodTicks(), b.rotationalPeriodTicks()); + } + // ─── Determinism ─────────────────────────────────────────────────────────── @Test From ab8e05f66378655dccd68ed36f98add89da31704 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 12 Aug 2026 10:11:52 +0300 Subject: [PATCH 07/42] fix: unswap the generator's atmosphere and distance, realize a moon as a moon - the legacy generator fed its atmosphere table into orbital distance - both quantities sit near 100, which is why the swap stayed invisible - a realized procedural moon had no parent, so isMoon was false forever - it stood at its parent's own orbit and every moon path was dead for it - take a moon's own distance from its ephemeris, the only place it lives --- .../dimension/DimensionManager.java | 13 +++++- .../universe/BodyEphemeris.java | 10 +++++ .../universe/PlanetRealizer.java | 30 ++++++++++++- .../advancedRocketry/universe/SystemBody.java | 5 +++ .../test/unit/PlanetRealizationTest.java | 43 +++++++++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java index 602321e14..0ab04200a 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java @@ -773,7 +773,12 @@ private List generateRandomPlanets(StellarBody star, int nu int baseAtm = 180; int baseDistance = 100; - DimensionProperties properties = DimensionManager.getInstance().generateRandomGasGiant(star.getId(), "", baseDistance + 50, baseAtm, 125, 100, 100, 75); + // Atmosphere first, then distance — the order the signature declares. These two arguments + // were swapped, and it was invisible because both quantities sit near 100 while meaning + // entirely different things (see AstronomicalBodyHelper's header: the distance, atmosphere + // and star-temperature scales are three separate 100s). A giant is thick-aired and far + // out; swapped, it came out thin-aired at 180 distance units. + DimensionProperties properties = DimensionManager.getInstance().generateRandomGasGiant(star.getId(), "", baseAtm, baseDistance + 50, 125, 100, 100, 75); dimPropList.add(properties); if (properties.gravitationalMultiplier >= 1f) { @@ -805,7 +810,11 @@ private List generateRandomPlanets(StellarBody star, int nu baseDistance = 30; } - DimensionProperties properties = DimensionManager.getInstance().generateRandom(star.getId(), baseDistance, baseAtm, 125, 100, 100, 75); + // Atmosphere first, then distance — see the gas-giant call above; the same two arguments + // were swapped here. The tables say what was meant: baseAtm is driven by i % 4 to 0 or 120 + // (an atmosphere table, including the airless world every fourth planet was to be), and + // baseDistance by i % 3 to 170 or 30 (a distance table). + DimensionProperties properties = DimensionManager.getInstance().generateRandom(star.getId(), baseAtm, baseDistance, 125, 100, 100, 75); if (properties == null) continue; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/BodyEphemeris.java b/src/main/java/zmaster587/advancedRocketry/universe/BodyEphemeris.java index 08e4e4eca..ed0a6af33 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/BodyEphemeris.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/BodyEphemeris.java @@ -72,6 +72,16 @@ public static BodyEphemeris orbit(double distUnits, double baseTheta, double phi } /** {@code true} iff this law is time-invariant — the degenerate frame of a star, or of a void cell. */ + /** + * The orbital distance this law was built with, in the caller's unit — for a moon, its distance + * from its PARENT, which lives nowhere else: {@code SystemBody.orbitalDistance()} deliberately + * holds the parent's distance from the star instead, because that is what a moon's climate + * depends on. Zero for a fixed law. + */ + public double distUnits() { + return distUnits; + } + public boolean isStatic() { return unitBlocks == 0L || !(periodTicks > 0d) || Double.isInfinite(periodTicks) || distUnits == 0d; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java index bc3496580..4ebdd6119 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java @@ -92,6 +92,7 @@ public static int realize(MinecraftServer server, GalacticCoord bodyCell) { List here = registry.bodiesAt(bodyCell); SystemBody target = null; + SystemBody parentBody = null; int variant = 0; int seen = 0; for (SystemBody body : here) { @@ -99,6 +100,12 @@ public static int realize(MinecraftServer server, GalacticCoord bodyCell) { || body.kind() == SystemBodyKind.ASTEROID_BELT) { continue; } + // A moon shares its parent's cell, and the scan below can only reach one once the parent + // already HAS a dimension (an unrealized parent would be picked as the target first), so + // the parent found here is always realizable into a link. + if (parentBody == null && body.kind() != SystemBodyKind.MOON) { + parentBody = body; + } // The variant is a body's rank among the worlds SHARING this cell, and it must be counted // exactly the way the generator assigned it — a planet is 0 and its moons follow — or a // realized moon would materialize a different world than the one that was scanned. @@ -137,7 +144,7 @@ public static int realize(MinecraftServer server, GalacticCoord bodyCell) { BodyProfile profile = PlanetDerivation.derive(registry.worldSeed(), anchor, target.name(), variant, star, target.kind() == SystemBodyKind.MOON, target.orbitalDistance()); - DimensionProperties props = materialize(dimId, profile, star, anchor, target); + DimensionProperties props = materialize(dimId, profile, star, anchor, target, parentBody); if (!DimensionManager.getInstance().registerDim(props, true)) { LOGGER.error("[UNIVERSE] dimension {} was already registered while realizing {}", dimId, @@ -162,12 +169,31 @@ public static int realize(MinecraftServer server, GalacticCoord bodyCell) { * {@code Random}. */ private static DimensionProperties materialize(int dimId, BodyProfile profile, StellarBody star, - GalacticCoord anchor, SystemBody body) { + GalacticCoord anchor, SystemBody body, + SystemBody parentBody) { DimensionProperties props = new DimensionProperties(dimId); props.setName(star.getName() + " " + dimId); props.setStar(star); props.orbitalDist = Math.max(DimensionProperties.MIN_DISTANCE, profile.orbitalDistance()); + // A MOON must be realized as a moon. Without this it became a planet standing at its parent's + // exact orbit forever, and every moon-specific path — the parent-mass period law, the moon sky, + // the moon branch of orbitThetaAt — was dead for it, because isMoon() answered false. + // Its own distance from the parent lives in its ephemeris; profile.orbitalDistance() is the + // PARENT's distance from the star, which is what its climate is derived from and must stay. + if (body != null && body.kind() == SystemBodyKind.MOON && parentBody != null + && parentBody.dimId() != Constants.INVALID_PLANET) { + DimensionProperties parentProps = + DimensionManager.getInstance().getDimensionProperties(parentBody.dimId()); + if (parentProps != null) { + int localOrbit = (int) Math.round(body.offsetLaw().distUnits()); + props.orbitalDist = Math.max(DimensionProperties.MIN_DISTANCE, localOrbit); + props.setParentPlanet(parentProps); + } else { + LOGGER.warn("[UNIVERSE] moon {} realized without a parent: dim {} has no properties", + body.name().cellKey(), parentBody.dimId()); + } + } // The orbital angle is READ OFF the body's cell rather than drawn again, so the planet the sky // shows and the planet the orbital elements describe are in the same place. props.baseOrbitTheta = angleOf(anchor, body.name()); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java index 08586963d..7fe115cbd 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java @@ -101,6 +101,11 @@ private static GalacticCoord requireAddress(GalacticCoord address) { * passing tick nor any amount of flight changes it, and membership of a cell is decided by * comparing these. */ + /** This body's motion law about its primary — see {@link BodyEphemeris#distUnits()}. */ + public BodyEphemeris offsetLaw() { + return offsetLaw; + } + public GalacticCoord name() { return name; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java index fdec9910c..5583fc1a7 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java @@ -87,6 +87,49 @@ public void theProceduralGalaxyOffersLandableBodiesThatHaveNoWorldYet() { } } + /** + * A moon carries TWO distances, and they are different numbers. + * + *

{@code SystemBody.orbitalDistance()} deliberately holds the PARENT's distance from the star, + * because that is what a moon's climate depends on. Its own distance from the parent lives in its + * ephemeris and nowhere else — which is exactly what realization needs to write into a moon's + * {@code orbitalDist}, since that field means "from my parent" for a moon. If the generator ever + * stops carrying it, a realized moon silently lands on top of its parent again.

+ */ + @Test + public void aMoonCarriesItsOwnDistanceFromItsParentSeparatelyFromItsParentsFromTheStar() { + UniverseRegistry reg = registryWithProceduralGalaxy(); + SystemBody moon = null; + SystemBody itsParent = null; + outer: + for (long x = -8; x <= 8 && moon == null; x++) { + for (long y = -8; y <= 8; y++) { + for (long z = -8; z <= 8; z++) { + SystemBody parent = null; + for (SystemBody b : reg.bodiesAt(GalacticCoord.ofSectorLocal(x, y, z, 0L, 0L, 0L))) { + if (parent == null && b.kind() != SystemBodyKind.MOON && b.kind().canDescend()) { + parent = b; + } else if (b.kind() == SystemBodyKind.MOON && parent != null) { + moon = b; + itsParent = parent; + break outer; + } + } + } + } + } + assertNotNull("the procedural galaxy must produce a moon to test with", moon); + assertNotNull(itsParent); + + double ownDistance = moon.offsetLaw().distUnits(); + assertTrue("a moon's own distance from its parent must be a real, positive number: " + ownDistance, + ownDistance > 0d); + assertEquals("a moon's orbitalDistance() is its PARENT's distance from the star", + itsParent.orbitalDistance(), moon.orbitalDistance()); + assertNotEquals("the two distances must not be the same number, or the seam is undetectable", + (double) moon.orbitalDistance(), ownDistance, 1e-9); + } + @Test public void realizingABodyMakesItADescentTargetAndRecordsItsCellName() { UniverseRegistry reg = registryWithProceduralGalaxy(); From d713a2bbb973c67846844294bfd321b943b80d79 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 12 Aug 2026 10:35:07 +0300 Subject: [PATCH 08/42] fix: a procedural planet orbits its star, and its moons travel with it - the convenience ctor pinned every procedural body to a static frame - the orbit belongs in the frame, as an authored system already has it - moons now ride the parent's frame instead of a static one of their own - share the cell-angle function so the frame and baseOrbitTheta agree - pin it with a test proven to fail on the previous code --- .../universe/ClusteredGalaxyGenerator.java | 24 +++++++-- .../universe/PlanetRealizer.java | 8 ++- .../test/unit/PlanetRealizationTest.java | 53 +++++++++++++++++++ 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index c93eb768d..c391aaaa9 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -11,6 +11,7 @@ import zmaster587.advancedRocketry.api.Constants; import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.space.AbsolutePos; import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; @@ -226,14 +227,26 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { // of being authored. Kept here rather than at realization because the nav list, the sky // and the descent trigger all read the kind long before anyone lands. BodyProfile profile = PlanetDerivation.derive(seed, cell, addr, 0, star, false, orbit); + // THE ORBIT LIVES IN THE FRAME, not in the body's own offset — the same shape an authored + // system uses (SystemContent: a planet sits at its frame origin and the FRAME goes round + // the star). Built with the convenience constructor, a procedural planet got + // CellFrame.staticAt(...) and a FIXED offset, so it stood still relative to its star + // forever while its own moons orbited it, and the identical system authored in XML moved. + double theta = PlanetRealizer.angleOf(cell, addr); + double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY + * AstronomicalBodyHelper.getOrbitalPeriod(orbit, star.getMass()); + CellFrame bodyFrame = CellFrame.of(AbsolutePos.ofCellName(cell.cellCentre()), + BodyEphemeris.orbit(orbit, theta, 0d, false, periodTicks, + SystemContent.ORBIT_UNIT_BLOCKS)); // Procedural bodies have no realized dimension yet — a descent (Layer 2) realizes one. - bodies.add(new SystemBody(addr, profile.kind(), Constants.INVALID_PLANET, starId, orbit)); + bodies.add(new SystemBody(addr, bodyFrame, BodyEphemeris.STATIC, profile.kind(), + Constants.INVALID_PLANET, starId, orbit)); outermostOrbit = Math.max(outermostOrbit, orbit); if (profile.kind() == SystemBodyKind.GAS_GIANT && (innermostGiantOrbit == 0 || orbit < innermostGiantOrbit)) { innermostGiantOrbit = orbit; } - addMoons(bodies, seed, cell, addr, orbit, star, starId, profile); + addMoons(bodies, seed, cell, addr, bodyFrame, orbit, star, starId, profile); } // An inner belt is DERIVED from a giant and never rolled: it is material a giant's resonances @@ -324,7 +337,8 @@ private static void addBelt(List bodies, long seed, GalacticCoord an * thing that actually positions it.

*/ private void addMoons(List bodies, long seed, GalacticCoord anchor, GalacticCoord parent, - int parentOrbit, StellarBody star, int starId, BodyProfile parentProfile) { + CellFrame parentFrame, int parentOrbit, StellarBody star, int starId, + BodyProfile parentProfile) { boolean giant = parentProfile.kind() == SystemBodyKind.GAS_GIANT; int max = giant ? MAX_MOONS_GIANT : MAX_MOONS_ROCKY; double u = CellHash.norm(CellHash.ofCell(seed, parent, SALT_MOONCOUNT)); @@ -347,7 +361,9 @@ private void addMoons(List bodies, long seed, GalacticCoord anchor, * AstronomicalBodyHelper.getMoonOrbitalPeriod(moonOrbit, (float) parentMass); BodyEphemeris law = BodyEphemeris.orbit(moonOrbit, theta, 0d, false, periodTicks, SystemContent.MOON_UNIT_BLOCKS); - bodies.add(new SystemBody(parent, CellFrame.staticAt(parent), law, SystemBodyKind.MOON, + // A moon rides its PARENT's frame, so a planet and its moons travel as one destination. + // It used to ride a static frame of its own, which pinned the whole family in place. + bodies.add(new SystemBody(parent, parentFrame, law, SystemBodyKind.MOON, Constants.INVALID_PLANET, starId, parentOrbit)); } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java index 4ebdd6119..2ee7c8983 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java @@ -273,7 +273,13 @@ private static int rotationalPeriodOf(BodyProfile profile, StellarBody star) { } /** The angle of a body's cell about its system's anchor, in radians. */ - private static double angleOf(GalacticCoord anchor, GalacticCoord bodyCell) { + /** + * A body's orbital angle, read off its cell rather than drawn again — so the sky, the orbital + * elements and the frame a body rides all put it in the same place. Shared with + * {@link ClusteredGalaxyGenerator}, which must build the frame from the same angle this writes + * into {@code baseOrbitTheta}; a second copy of this arithmetic would let the two drift. + */ + static double angleOf(GalacticCoord anchor, GalacticCoord bodyCell) { long dx = bodyCell.sectorX() - anchor.sectorX(); long dz = bodyCell.sectorZ() - anchor.sectorZ(); if (dx == 0L && dz == 0L) { diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java index 5583fc1a7..e40728ef5 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java @@ -130,6 +130,59 @@ public void aMoonCarriesItsOwnDistanceFromItsParentSeparatelyFromItsParentsFromT (double) moon.orbitalDistance(), ownDistance, 1e-9); } + /** + * A procedural planet ORBITS its star, and its moons travel with it. + * + *

This was false: the convenience {@code SystemBody(address, kind, dimId, starId, orbit)} + * constructor hard-wires a static frame and a fixed offset, so every procedural planet stood + * still relative to its star forever — while its own moons orbited it, and while the identical + * system authored in XML moved. Nothing pinned it, which is why it survived.

+ * + *

Two assertions, because either alone can be satisfied by the wrong thing: the planet must + * MOVE, and the moon must stay NEAR it while it does. A moon on its own static frame would leave + * its planet behind; a planet that only moved because its moon's law leaked into it would drag + * the separation open.

+ */ + @Test + public void aProceduralPlanetOrbitsItsStarAndItsMoonsTravelWithIt() { + UniverseRegistry reg = registryWithProceduralGalaxy(); + SystemBody planet = null; + SystemBody moon = null; + outer: + for (long x = -8; x <= 8; x++) { + for (long y = -8; y <= 8; y++) { + for (long z = -8; z <= 8; z++) { + SystemBody candidate = null; + for (SystemBody b : reg.bodiesAt(GalacticCoord.ofSectorLocal(x, y, z, 0L, 0L, 0L))) { + if (candidate == null && b.kind() != SystemBodyKind.MOON && b.kind().canDescend()) { + candidate = b; + } else if (b.kind() == SystemBodyKind.MOON && candidate != null) { + planet = candidate; + moon = b; + break outer; + } + } + } + } + } + assertNotNull("the procedural galaxy must produce a planet with a moon", planet); + assertNotNull(moon); + + // One Earth-like year of ticks. A body at any orbit this generator produces turns by a + // substantial fraction of a revolution in that time, so "did it move" is not a rounding test. + long later = 24000L * 48L; + double planetTravelled = planet.absoluteAt(0L).minus(planet.absoluteAt(later)).length(); + assertTrue("a procedural planet must go round its star, not stand at a fixed point" + + " (it moved " + planetTravelled + " blocks in a year)", planetTravelled > 1000d); + + double separationNow = planet.absoluteAt(0L).minus(moon.absoluteAt(0L)).length(); + double separationLater = planet.absoluteAt(later).minus(moon.absoluteAt(later)).length(); + assertTrue("a moon must ride its parent's frame, so their separation stays a moon's orbit" + + " wide while both travel (" + separationNow + " -> " + separationLater + + ", planet moved " + planetTravelled + ")", + separationLater < planetTravelled / 2d); + } + @Test public void realizingABodyMakesItADescentTargetAndRecordsItsCellName() { UniverseRegistry reg = registryWithProceduralGalaxy(); From 0e8c7a960f004cac3958b8f3f2be5b48a1f74795 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 12 Aug 2026 10:48:31 +0300 Subject: [PATCH 09/42] refactor: a body that does not move must say so - delete the SystemBody constructors that silently chose a static frame - replace them with SystemBody.fixedAt, which names the choice - the two production users are a star at its anchor and a belt round it - 38 call sites converted; javac enumerated every one --- .../command/test/TestProbeCommand.java | 3 +- .../universe/ClusteredGalaxyGenerator.java | 6 ++-- .../advancedRocketry/universe/SystemBody.java | 23 ++++++++++---- .../test/unit/PlanetRealizationTest.java | 2 +- .../test/unit/SystemBodiesProducerTest.java | 28 ++++++++--------- .../test/unit/SystemBodyTest.java | 30 +++++++++---------- .../test/unit/TelescopeRegionScanTest.java | 8 ++--- .../test/unit/UniverseRegistryTest.java | 8 ++--- 8 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index baf61f4ff..7389a50fd 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -3886,8 +3886,9 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] int starId = parseIntOr(args[9], 0); zmaster587.advancedRocketry.space.GalacticCoord coord = zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal(sx, sy, sz, lx, ly, lz); + // A POI planted by hand does not move — say so, rather than letting a constructor decide. zmaster587.advancedRocketry.universe.SystemBody body = - new zmaster587.advancedRocketry.universe.SystemBody(coord, kind, dimId, starId); + zmaster587.advancedRocketry.universe.SystemBody.fixedAt(coord, kind, dimId, starId); reg.addPoi(body); send(sender, "{\"ok\":true,\"cellKey\":\"" + coord.cellKey() + "\",\"descendTarget\":" + body.isDescendTarget() + "}"); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index c391aaaa9..1a8d6da8f 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -193,7 +193,8 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { StellarBody star = sys.get().star(); List bodies = new ArrayList<>(); // The star sits at the anchor cell's centre. - bodies.add(new SystemBody(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, starId)); + // A star does not move inside its own system: its frame IS the system's anchor. + bodies.add(SystemBody.fixedAt(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, starId)); // Bodies orbit at cell-scale radii: min 1 cell out (never the anchor cell), max = the bounded // neighbourhood radius. The anchor sits in the middle band of its super-cell (>= 3s/8 from every @@ -321,7 +322,8 @@ private static void addBelt(List bodies, long seed, GalacticCoord an int clamped = Math.max(1, orbit); GalacticCoord addr = placeBody(seed, anchor, index, clamped, star, s, taken); if (addr != null) { - bodies.add(new SystemBody(addr, SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, + // A belt is centred on the star it rings, so as a whole it does not travel round it. + bodies.add(SystemBody.fixedAt(addr, SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, starId, clamped)); } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java index 7fe115cbd..a6e033ede 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java @@ -55,14 +55,27 @@ public final class SystemBody { * without a system to ride. {@code address}'s sector triple becomes the name and its local offset * the (constant) in-cell offset. */ - public SystemBody(GalacticCoord address, SystemBodyKind kind, int dimId, int starId) { - this(address, kind, dimId, starId, ORBIT_UNKNOWN); + /** + * A body that DOES NOT MOVE — pinned to a static frame at its own cell, forever. + * + *

Named rather than offered as a plain constructor on purpose. This used to be + * {@code new SystemBody(address, kind, dimId, starId, orbit)}, and it read like the ordinary way + * to make a body while silently choosing immobility: the procedural generator built every planet + * through it, so a whole galaxy of worlds stood still relative to their stars while the same + * systems authored in XML orbited. A body that does not move is a real and legitimate thing — a + * star at its own system's anchor, a belt centred on that star — but it is a CHOICE, and the + * choice now has to be spelled.

+ * + *

For a body that moves, pass its {@link CellFrame} and {@link BodyEphemeris} explicitly.

+ */ + public static SystemBody fixedAt(GalacticCoord address, SystemBodyKind kind, int dimId, int starId) { + return fixedAt(address, kind, dimId, starId, ORBIT_UNKNOWN); } /** The same, carrying the body's orbital radius — see {@link #orbitalDistance()}. */ - public SystemBody(GalacticCoord address, SystemBodyKind kind, int dimId, int starId, - int orbitalDistance) { - this(requireAddress(address).cellCentre(), CellFrame.staticAt(address), + public static SystemBody fixedAt(GalacticCoord address, SystemBodyKind kind, int dimId, int starId, + int orbitalDistance) { + return new SystemBody(requireAddress(address).cellCentre(), CellFrame.staticAt(address), BodyEphemeris.fixed(address.localX(), address.localY(), address.localZ()), kind, dimId, starId, orbitalDistance); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java index e40728ef5..5531c6dff 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java @@ -332,7 +332,7 @@ public void aProceduralBodyCarriesTheOrbitItsPhysicsWasDerivedFrom() { @Test public void theOrbitSurvivesAnNbtRoundTrip() { - SystemBody body = new SystemBody(GalacticCoord.ofSectorLocal(3, 4, 5, 0, 0, 0), + SystemBody body = SystemBody.fixedAt(GalacticCoord.ofSectorLocal(3, 4, 5, 0, 0, 0), SystemBodyKind.PLANET, 12, -7, 1234); net.minecraft.nbt.NBTTagCompound nbt = new net.minecraft.nbt.NBTTagCompound(); body.writeToNBT(nbt); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodiesProducerTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodiesProducerTest.java index ac18a620c..41eaf37cb 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodiesProducerTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodiesProducerTest.java @@ -67,7 +67,7 @@ public void aBodyAtTheCellCentreIsCarriedAsTheDirectionFromTheShipThatIsThere() // Ship parked OFF the cell centre; a planet sitting AT the cell centre (local 0,0,0). GalacticCoord ship = GalacticCoord.ofSectorLocal(0L, 0L, 0L, 100L, 50L, -30L); GalacticCoord planet = GalacticCoord.ofSectorLocal(0L, 0L, 0L, 0L, 0L, 0L); - SystemBody body = new SystemBody(planet, SystemBodyKind.PLANET, 3, 7); + SystemBody body = SystemBody.fixedAt(planet, SystemBodyKind.PLANET, 3, 7); ShipLedger ledger = new ShipLedger(); ledger.settle(UUID.randomUUID(), ship); @@ -97,7 +97,7 @@ public void crossCellBodyDirectionIncludesTheSectorTerm() { // just the local delta (documents the component-wise sector-aware formula). GalacticCoord ship = GalacticCoord.ofSectorLocal(0L, 0L, 0L, 100L, 0L, 0L); GalacticCoord body = GalacticCoord.ofSectorLocal(1L, 0L, 0L, 0L, 0L, 0L); - SystemBody star = new SystemBody(body, SystemBodyKind.STAR, Constants.INVALID_PLANET, 7); + SystemBody star = SystemBody.fixedAt(body, SystemBodyKind.STAR, Constants.INVALID_PLANET, 7); ShipLedger ledger = new ShipLedger(); ledger.settle(UUID.randomUUID(), ship); @@ -115,7 +115,7 @@ public void crossCellBodyDirectionIncludesTheSectorTerm() { public void nonDescendBodyCarriesDescendTargetFalse() { GalacticCoord ship = GalacticCoord.ofSectorLocal(0L, 0L, 0L, 5L, 0L, 0L); GalacticCoord beltCoord = GalacticCoord.ofSectorLocal(0L, 0L, 0L, 0L, 0L, 0L); - SystemBody belt = new SystemBody(beltCoord, SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, 7); + SystemBody belt = SystemBody.fixedAt(beltCoord, SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, 7); ShipLedger ledger = new ShipLedger(); ledger.settle(UUID.randomUUID(), ship); @@ -137,7 +137,7 @@ public void aLiveCellWhoseOnlyShipIsMidJumpStillShowsItsBodies() { // one of those bodies vanished from his sky and the blank was indistinguishable from a void. GalacticCoord cell = GalacticCoord.ofSectorLocal(57L, 0L, 5L, 0L, 0L, 0L); GalacticCoord shipPos = GalacticCoord.ofSectorLocal(57L, 0L, 5L, 125L, 0L, -1016L); - SystemBody moon = new SystemBody(GalacticCoord.ofSectorLocal(57L, 0L, 5L, 2900L, 0L, 0L), + SystemBody moon = SystemBody.fixedAt(GalacticCoord.ofSectorLocal(57L, 0L, 5L, 2900L, 0L, 0L), SystemBodyKind.MOON, 4, 7); ShipLedger ledger = new ShipLedger(); @@ -161,7 +161,7 @@ public void aShipMidJumpKeysNoDimensionOfItsOwn() { ShipLedger ledger = new ShipLedger(); ledger.beginTransit(UUID.randomUUID(), GalacticCoord.ofSectorLocal(0L, 0L, 0L, 0L, 0L, 0L)); - SystemBody body = new SystemBody(GalacticCoord.ORIGIN, SystemBodyKind.PLANET, 3, 7); + SystemBody body = SystemBody.fixedAt(GalacticCoord.ORIGIN, SystemBodyKind.PLANET, 3, 7); BodyLookup always = new BodyLookup() { @Override public List skyBodiesAt(GalacticCoord cell) { @@ -180,7 +180,7 @@ public void aLiveCellWithNoShipInItIsStillFedFromItsCentre() { // member whose ship departed without him, a passenger, a player put there by an on-ramp). His // sky is the cell's, measured from the only point that is his if no ship is: the cell centre. GalacticCoord cell = GalacticCoord.ofSectorLocal(4L, 1L, 2L, 0L, 0L, 0L); - SystemBody planet = new SystemBody(GalacticCoord.ofSectorLocal(4L, 1L, 2L, 0L, 5000L, 0L), + SystemBody planet = SystemBody.fixedAt(GalacticCoord.ofSectorLocal(4L, 1L, 2L, 0L, 5000L, 0L), SystemBodyKind.PLANET, 3, 7); Map> byDim = SystemBodiesProducer.buildByDim( @@ -199,7 +199,7 @@ public void aSettledShipIsPreferredOverAParkedOneAsTheObserver() { GalacticCoord cell = GalacticCoord.ofSectorLocal(0L, 0L, 0L, 0L, 0L, 0L); GalacticCoord settledAt = GalacticCoord.ofSectorLocal(0L, 0L, 0L, 700L, 0L, 0L); GalacticCoord inboundTo = GalacticCoord.ofSectorLocal(0L, 0L, 0L, -900L, 0L, 0L); - SystemBody planet = new SystemBody(cell, SystemBodyKind.PLANET, 3, 7); + SystemBody planet = SystemBody.fixedAt(cell, SystemBodyKind.PLANET, 3, 7); ShipLedger ledger = new ShipLedger(); ledger.beginTransit(UUID.randomUUID(), inboundTo); @@ -238,9 +238,9 @@ public List skyBodiesAt(GalacticCoord cell) { public void everyLiveCellKeysItsOwnSlotDim() { GalacticCoord shipA = GalacticCoord.ofSectorLocal(0L, 0L, 0L, 10L, 0L, 0L); GalacticCoord shipB = GalacticCoord.ofSectorLocal(5L, 0L, 0L, 0L, 0L, 0L); - final SystemBody planetA = new SystemBody(GalacticCoord.ofSectorLocal(0L, 0L, 0L, 0L, 0L, 0L), + final SystemBody planetA = SystemBody.fixedAt(GalacticCoord.ofSectorLocal(0L, 0L, 0L, 0L, 0L, 0L), SystemBodyKind.PLANET, 3, 7); - final SystemBody planetB = new SystemBody(GalacticCoord.ofSectorLocal(5L, 0L, 0L, 0L, 0L, 0L), + final SystemBody planetB = SystemBody.fixedAt(GalacticCoord.ofSectorLocal(5L, 0L, 0L, 0L, 0L, 0L), SystemBodyKind.MOON, 4, 7); ShipLedger ledger = new ShipLedger(); @@ -276,7 +276,7 @@ public void aShipWhoseCellIsInNoSlotContributesNothing() { // dimension to key its sky under, and the only wrong answer is to invent one: keying the feed // to a stale or borrowed id points a cell's bodies at a world holding somebody else's cell. GalacticCoord ship = GalacticCoord.ofSectorLocal(4L, 0L, 0L, 0L, 0L, 0L); - SystemBody planet = new SystemBody(ship, SystemBodyKind.PLANET, 3, 7); + SystemBody planet = SystemBody.fixedAt(ship, SystemBodyKind.PLANET, 3, 7); ShipLedger ledger = new ShipLedger(); ledger.settle(UUID.randomUUID(), ship); @@ -296,7 +296,7 @@ public void anUnboundOrMalformedBindingIsNeverKeyed() { // The "no world" sentinel must never become a dimension key, and neither must a cell key the // coordinate parser cannot read back - both would put a body list under an id nothing renders. GalacticCoord cell = GalacticCoord.ofSectorLocal(1L, 1L, 1L, 0L, 0L, 0L); - SystemBody planet = new SystemBody(cell, SystemBodyKind.PLANET, 3, 7); + SystemBody planet = SystemBody.fixedAt(cell, SystemBodyKind.PLANET, 3, 7); Map hostile = new LinkedHashMap<>(); hostile.put(cell.cellKey(), SpaceManager.UNBOUND_SLOT); @@ -393,8 +393,8 @@ public AbsolutePos originAt(GalacticCoord name, long tick) { @Test public void anAuthoredStarIsFedItsOwnProxyDimensionAndAProceduralOneIsNot() { GalacticCoord cell = GalacticCoord.ORIGIN; - SystemBody authored = new SystemBody(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, 4); - SystemBody procedural = new SystemBody(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, -9); + SystemBody authored = SystemBody.fixedAt(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, 4); + SystemBody procedural = SystemBody.fixedAt(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, -9); GalacticCoord ship = GalacticCoord.ofSectorLocal(0L, 0L, 0L, 500L, 0L, 0L); ShipLedger ledger = new ShipLedger(); ledger.settle(UUID.randomUUID(), ship); @@ -419,7 +419,7 @@ public void nullInputsYieldEmptyMap() { .isEmpty()); // A missing ledger is NOT a missing feed: the cell is live, so its sky is drawn - from the // cell centre, because there is no ship to measure it from. - SystemBody planet = new SystemBody(GalacticCoord.ofSectorLocal(0L, 0L, 0L, 0L, 800L, 0L), + SystemBody planet = SystemBody.fixedAt(GalacticCoord.ofSectorLocal(0L, 0L, 0L, 0L, 800L, 0L), SystemBodyKind.PLANET, 3, 7); Map> byDim = SystemBodiesProducer.buildByDim(live(cell, 1), null, lookupIn(cell, planet)); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodyTest.java index c3d603551..e8371b2aa 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodyTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodyTest.java @@ -31,7 +31,7 @@ private static BodyEphemeris orbit(double distUnits, long unitBlocks) { @Test public void nbtRoundTripPreservesEveryField() { - SystemBody body = new SystemBody(GalacticCoord.ofSectorLocal(4, -5, 6, 123_456, -7_890, 42), + SystemBody body = SystemBody.fixedAt(GalacticCoord.ofSectorLocal(4, -5, 6, 123_456, -7_890, 42), SystemBodyKind.STATION_SLOT, 815, -12345); NBTTagCompound tag = new NBTTagCompound(); body.writeToNBT(tag); @@ -126,7 +126,7 @@ public void aBodyStillMovesAfterAnNbtRoundTrip() { @Test public void aBodyRebindsToTheFrameOfTheCellItIsServedFrom() { GalacticCoord name = GalacticCoord.ofSectorLocal(3, 0, 0, 5_000, 0, 0); - SystemBody station = new SystemBody(name, SystemBodyKind.STATION_SLOT, + SystemBody station = SystemBody.fixedAt(name, SystemBodyKind.STATION_SLOT, Constants.INVALID_PLANET, 0); assertEquals("a bare POI stands still", station.absoluteAt(0L), station.absoluteAt(500L)); @@ -142,32 +142,32 @@ public void aBodyRebindsToTheFrameOfTheCellItIsServedFrom() { @Test public void onlyRealBodiesDefineACellsFrame() { GalacticCoord at = GalacticCoord.ORIGIN; - assertTrue(new SystemBody(at, SystemBodyKind.STAR, Constants.INVALID_PLANET, 0).definesFrame()); - assertTrue(new SystemBody(at, SystemBodyKind.PLANET, 1, 0).definesFrame()); - assertTrue(new SystemBody(at, SystemBodyKind.GAS_GIANT, 2, 0).definesFrame()); - assertTrue(new SystemBody(at, SystemBodyKind.ASTEROID_BELT, + assertTrue(SystemBody.fixedAt(at, SystemBodyKind.STAR, Constants.INVALID_PLANET, 0).definesFrame()); + assertTrue(SystemBody.fixedAt(at, SystemBodyKind.PLANET, 1, 0).definesFrame()); + assertTrue(SystemBody.fixedAt(at, SystemBodyKind.GAS_GIANT, 2, 0).definesFrame()); + assertTrue(SystemBody.fixedAt(at, SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, 0).definesFrame()); - assertFalse(new SystemBody(at, SystemBodyKind.MOON, 3, 0).definesFrame()); - assertFalse(new SystemBody(at, SystemBodyKind.STATION_SLOT, + assertFalse(SystemBody.fixedAt(at, SystemBodyKind.MOON, 3, 0).definesFrame()); + assertFalse(SystemBody.fixedAt(at, SystemBodyKind.STATION_SLOT, Constants.INVALID_PLANET, 0).definesFrame()); } @Test public void descendTargetOnlyForPlanetOrMoonWithARealDimension() { GalacticCoord at = GalacticCoord.ofSectorLocal(1, 1, 1, 10, 20, 30); - assertTrue(new SystemBody(at, SystemBodyKind.PLANET, 7, 1).isDescendTarget()); - assertTrue(new SystemBody(at, SystemBodyKind.MOON, 8, 1).isDescendTarget()); + assertTrue(SystemBody.fixedAt(at, SystemBodyKind.PLANET, 7, 1).isDescendTarget()); + assertTrue(SystemBody.fixedAt(at, SystemBodyKind.MOON, 8, 1).isDescendTarget()); assertFalse("a planet with no realized dim is not yet a descent target", - new SystemBody(at, SystemBodyKind.PLANET, Constants.INVALID_PLANET, 1).isDescendTarget()); - assertFalse(new SystemBody(at, SystemBodyKind.STAR, Constants.INVALID_PLANET, 1).isDescendTarget()); - assertFalse(new SystemBody(at, SystemBodyKind.STATION_SLOT, Constants.INVALID_PLANET, 1).isDescendTarget()); - assertFalse(new SystemBody(at, SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, 1).isDescendTarget()); + SystemBody.fixedAt(at, SystemBodyKind.PLANET, Constants.INVALID_PLANET, 1).isDescendTarget()); + assertFalse(SystemBody.fixedAt(at, SystemBodyKind.STAR, Constants.INVALID_PLANET, 1).isDescendTarget()); + assertFalse(SystemBody.fixedAt(at, SystemBodyKind.STATION_SLOT, Constants.INVALID_PLANET, 1).isDescendTarget()); + assertFalse(SystemBody.fixedAt(at, SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, 1).isDescendTarget()); } @Test public void unknownKindDecodesToAnInertPoiRatherThanCrashing() { NBTTagCompound tag = new NBTTagCompound(); - new SystemBody(GalacticCoord.ORIGIN, SystemBodyKind.PLANET, 5, 1).writeToNBT(tag); + SystemBody.fixedAt(GalacticCoord.ORIGIN, SystemBodyKind.PLANET, 5, 1).writeToNBT(tag); tag.setString("kind", "SOME_FUTURE_KIND"); // a kind this version doesn't know SystemBody round = SystemBody.readFromNBT(tag); assertEquals(SystemBodyKind.STATION_SLOT, round.kind()); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java index 9237dda6a..4da4cbd11 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java @@ -196,14 +196,14 @@ private UniverseRegistry threeSystems() { UniverseRegistry registry = new UniverseRegistry(); registry.place(cell(4, 0, 0), 4); - registry.addPoi(new SystemBody(cell(4, 0, 0), SystemBodyKind.STAR, Constants.INVALID_PLANET, 4)); - registry.addPoi(new SystemBody(cell(4, 0, 0), SystemBodyKind.PLANET, 401, 4)); + registry.addPoi(SystemBody.fixedAt(cell(4, 0, 0), SystemBodyKind.STAR, Constants.INVALID_PLANET, 4)); + registry.addPoi(SystemBody.fixedAt(cell(4, 0, 0), SystemBodyKind.PLANET, 401, 4)); registry.place(cell(5, 1, 0), 5); - registry.addPoi(new SystemBody(cell(5, 1, 0), SystemBodyKind.PLANET, 501, 5)); + registry.addPoi(SystemBody.fixedAt(cell(5, 1, 0), SystemBodyKind.PLANET, 501, 5)); registry.place(cell(9, 0, 0), 9); - registry.addPoi(new SystemBody(cell(9, 0, 0), SystemBodyKind.PLANET, 901, 9)); + registry.addPoi(SystemBody.fixedAt(cell(9, 0, 0), SystemBodyKind.PLANET, 901, 9)); return registry; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java index 67e9ab131..5ac4ddd5d 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java @@ -472,9 +472,9 @@ public void worldSeedIsTransientAndNotPersisted() { public void poiStoreRoundTripsThroughNbt() { UniverseRegistry source = new UniverseRegistry(); GalacticCoord sys = GalacticCoord.ofSectorLocal(3, 3, 3, 0, 0, 0); - source.addPoi(new SystemBody(GalacticCoord.ofSectorLocal(3, 3, 3, 50_000, 0, 0), + source.addPoi(SystemBody.fixedAt(GalacticCoord.ofSectorLocal(3, 3, 3, 50_000, 0, 0), SystemBodyKind.STATION_SLOT, Constants.INVALID_PLANET, 7)); - source.addPoi(new SystemBody(GalacticCoord.ofSectorLocal(3, 3, 3, -20_000, 10_000, 0), + source.addPoi(SystemBody.fixedAt(GalacticCoord.ofSectorLocal(3, 3, 3, -20_000, 10_000, 0), SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, 7)); assertTrue("adding a POI must mark dirty", source.isDirty()); @@ -514,7 +514,7 @@ public void bodiesAtMergesProceduralBodiesAndPois() { assertFalse("a procedural system must have bodies", procedural.isEmpty()); int before = procedural.size(); - reg.addPoi(new SystemBody( + reg.addPoi(SystemBody.fixedAt( GalacticCoord.ofSectorLocal(found.sectorX(), found.sectorY(), found.sectorZ(), 100_000, 0, 0), SystemBodyKind.STATION_SLOT, Constants.INVALID_PLANET, -5)); List merged = reg.bodiesAt(found); @@ -704,7 +704,7 @@ public void theSkyFeedUnionsTheSystemWithTheObserversOwnCell() { assertTrue("the fixture's void cell must belong to the system", reg.anchorForCell(voidCell).isPresent()); assertTrue("...and hold no body of its own", reg.bodiesAt(voidCell).isEmpty()); - reg.addPoi(new SystemBody(voidCell.plusLocalSaturating(1_000L, 0L, 0L), + reg.addPoi(SystemBody.fixedAt(voidCell.plusLocalSaturating(1_000L, 0L, 0L), SystemBodyKind.STATION_SLOT, Constants.INVALID_PLANET, 6007)); List sky = reg.skyBodiesAt(voidCell); From 5b2aabb51dc9fdc8886ae78a274582502659d026 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 12 Aug 2026 13:58:20 +0300 Subject: [PATCH 10/42] fix: a probe that mounts a pilot must say which ship - seat-mount takes an optional near [maxDist] selector - the seat is resolved through the owning ship chunk claim - every reply carries seatsLoaded, so the bare form is visible - shipIdOwningBlock answers identity, not proximity --- .../command/test/TestProbeCommand.java | 47 ++++++++++++++++--- .../integration/vs/VSBridge.java | 15 ++++++ .../integration/vs/VSIntegration.java | 16 +++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 7389a50fd..1f26bdeac 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -1135,24 +1135,58 @@ world, parseDoubleOr(args[2], 0), parseDoubleOr(args[3], 0), + "}"); return; } - // seat-mount — spawn the pilot seat's dummy mount and return its entity id, so a - // test bot can `player mount-entity ` and become the ship's pilot. Mirrors - // BlockPilotSeat.onBlockActivated server-side (the bot cannot right-click a ship block). + // seat-mount [near [maxDist]] — spawn the pilot seat's dummy mount and + // return its entity id, so a test bot can `player mount-entity ` and become the ship's + // pilot. Mirrors BlockPilotSeat.onBlockActivated server-side (the bot cannot right-click a + // ship block). Without "near" the FIRST loaded seat answers, which is only defensible on a + // world holding one ship — the reply carries "seatsLoaded" so a caller can see when it is not. if (args.length >= 2 && "seat-mount".equalsIgnoreCase(args[0])) { net.minecraft.world.WorldServer world = vsWorld(sender, parseIntOr(args[1], Integer.MIN_VALUE)); if (world == null) { send(sender, "{\"error\":\"world not loaded\"}"); return; } - zmaster587.advancedRocketry.tile.TilePilotSeat seat = null; + // WHICH seat. The bare form takes the first loaded one, and on a world holding several + // ships that is a coin toss reported as a fact: it once mounted a pilot onto a ship + // 16,000,000 blocks away from the one under test, and the reply was indistinguishable + // from success. So the seat COUNT now travels in every reply, and a caller that means + // one particular ship names it with "near [maxDist]" — resolved through that + // ship's chunk CLAIM, an identity, rather than through whichever seat is nearest. + java.util.List seats = + new java.util.ArrayList<>(); for (TileEntity te : world.loadedTileEntityList) { if (te instanceof zmaster587.advancedRocketry.tile.TilePilotSeat) { - seat = (zmaster587.advancedRocketry.tile.TilePilotSeat) te; + seats.add((zmaster587.advancedRocketry.tile.TilePilotSeat) te); + } + } + String wantShipId = null; + if (args.length >= 6 && "near".equalsIgnoreCase(args[2])) { + double maxDist = args.length >= 7 + ? parseDoubleOr(args[6], Double.POSITIVE_INFINITY) : Double.POSITIVE_INFINITY; + wantShipId = zmaster587.advancedRocketry.integration.vs.VSIntegration.nearestShipId( + world, parseDoubleOr(args[3], 0), parseDoubleOr(args[4], 0), + parseDoubleOr(args[5], 0), maxDist); + if (wantShipId == null) { + send(sender, "{\"seatFound\":false,\"reason\":\"no loaded ship near that point\"" + + ",\"seatsLoaded\":" + seats.size() + "}"); + return; + } + } + zmaster587.advancedRocketry.tile.TilePilotSeat seat = null; + for (zmaster587.advancedRocketry.tile.TilePilotSeat candidate : seats) { + if (wantShipId == null) { + seat = candidate; + break; + } + if (wantShipId.equals(zmaster587.advancedRocketry.integration.vs.VSIntegration + .shipIdOwningBlock(world, candidate.getPos()))) { + seat = candidate; break; } } if (seat == null) { - send(sender, "{\"seatFound\":false}"); + send(sender, "{\"seatFound\":false,\"seatsLoaded\":" + seats.size() + + (wantShipId == null ? "" : ",\"wantedShip\":\"" + wantShipId + "\"") + "}"); return; } BlockPos sp = seat.getPos(); @@ -1169,6 +1203,7 @@ world, parseDoubleOr(args[2], 0), parseDoubleOr(args[3], 0), } send(sender, "{\"seatFound\":true,\"dummyId\":" + dummy.getEntityId() + ",\"reused\":" + reused + + ",\"seatsLoaded\":" + seats.size() + ",\"seatX\":" + sp.getX() + ",\"seatY\":" + sp.getY() + ",\"seatZ\":" + sp.getZ() + "}"); return; } diff --git a/src/main/java/zmaster587/advancedRocketry/integration/vs/VSBridge.java b/src/main/java/zmaster587/advancedRocketry/integration/vs/VSBridge.java index 48551ab6f..bada0b212 100644 --- a/src/main/java/zmaster587/advancedRocketry/integration/vs/VSBridge.java +++ b/src/main/java/zmaster587/advancedRocketry/integration/vs/VSBridge.java @@ -719,6 +719,21 @@ static String nearestShipId(World world, double x, double y, double z, double ma return physo == null ? null : physo.getShipData().getUuid().toString(); } + /** + * The IDENTITY of the ship that owns a SUBSPACE block position — its VS ship uuid as a string — + * or {@code null} when the position belongs to no loaded ship. + * + *

This is the inverse of {@link #nearestShipId}: it answers from the ship's chunk CLAIM, which + * contains the block or does not, rather than from a distance that is merely small. A caller + * holding a block of a ship (a seat, a controller, a hatch) uses this to say WHICH ship it is a + * block of, on a world where several ships exist and their subspace yards sit side by side.

+ */ + static String shipIdOwningBlock(World world, net.minecraft.util.math.BlockPos pos) { + return ValkyrienUtils.getPhysoManagingBlock(world, pos) + .map(physo -> physo.getShipData().getUuid().toString()) + .orElse(null); + } + /** * State of the loaded ship with this uuid, in the same layout as {@link #nearestShipState}, or * {@code null} when the id names no ship that is loaded here (unloaded, deleted, another world, diff --git a/src/main/java/zmaster587/advancedRocketry/integration/vs/VSIntegration.java b/src/main/java/zmaster587/advancedRocketry/integration/vs/VSIntegration.java index 08d0ec5de..b66b0b47a 100644 --- a/src/main/java/zmaster587/advancedRocketry/integration/vs/VSIntegration.java +++ b/src/main/java/zmaster587/advancedRocketry/integration/vs/VSIntegration.java @@ -517,6 +517,22 @@ public static int shipBlockHeight(World world, double x, double y, double z) { * only this one ship, so any non-air block is a ship block. Shared by {@link #crossShip} (tight cut) * and {@link #shipBlockHeight}. */ + /** + * The identity (VS ship uuid, as a string) of the ship that OWNS a subspace block position, or + * {@code null} when VS is absent or the position belongs to no loaded ship. + * + *

Answered from the ship's chunk claim — which contains the block or does not — so it is an + * identity and not a proximity. A caller holding one block of a ship (a pilot seat, a hatch) uses + * this to say which ship that block belongs to on a world where several ships are loaded at once + * and their subspace yards are neighbours.

+ */ + public static String shipIdOwningBlock(World world, net.minecraft.util.math.BlockPos pos) { + if (!isAvailable()) { + return null; + } + return VSBridge.shipIdOwningBlock(world, pos); + } + /** * A single subspace block position of the VS ship whose world BB contains {@code (x,y,z)}, or * {@code null} when VS is absent / no ship is there / its shipyard is empty. Located through the From 73b70d4d7f196abf713dc22adb2f225c4ff97a16 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 12 Aug 2026 13:58:38 +0300 Subject: [PATCH 11/42] test: measure the render, the wire and a ship at far coordinates - both client spikes delivered into the reserved quadrant and never ran - render shows zero repeated frames out to 24M against an origin control - a sub-block position survives the round trip exactly at every rung - a ship assembled at 16M flies and keeps its rider like one at zero - the same holds with its blocks at subspace 19.2M --- .../SpikeFarCoordinateRenderJitterTest.java | 369 ++++++++++ .../client/SpikeFarCoordinateShipTest.java | 665 ++++++++++++++++++ .../SpikeSubBlockPositionGranularityTest.java | 298 ++++++++ 3 files changed, 1332 insertions(+) create mode 100644 src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateShipTest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/client/SpikeSubBlockPositionGranularityTest.java diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java new file mode 100644 index 000000000..71318b892 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java @@ -0,0 +1,369 @@ +package zmaster587.advancedRocketry.test.client; + +import com.github.stannismod.forge.testing.junit.AbstractClientE2ETest; +import com.google.gson.JsonObject; + +import org.junit.Test; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertTrue; + +/** + * SPIKE — is the render actually quantized far from the origin, the thing the 4M cell was sized for? + * + *

`space-model.md` justifies `CELL = 4_000_000` with "entity doubles / chunks / lighting degrade + * past ~±2M blocks in 1.12.2". The server half of that (chunk generation, block storage) measured + * CLEAN out to 28M. This measures the visual half. + * + *

The stimulus, and why it is motion rather than a still frame

+ * A float quantum does not produce a shimmer in a static scene — the error is CONSTANT, so a still + * camera at 16M renders a still (if slightly displaced) image. Quantization shows up when the camera + * moves by LESS than the quantum: the frame then refuses to change until the accumulated motion + * crosses one step. So the camera is walked in {@value #STEP_BLOCKS}-block increments and the metric + * is how many consecutive frames come back byte-identical. + * + *

Expected, if the render path carried absolute coordinates in float: at ±2M the quantum is 0.25 + * block, so ~5 frames repeat per step; at ±16M it is 2 blocks, so ~40 repeat. Expected, if the path + * subtracts the viewer position in double before casting (which is what + * {@code RenderManager.renderEntityStatic} and Valkyrien Skies' {@code PhysObjectRenderManager} both + * appear to do): zero repeats at every coordinate. + * + *

Two controls, because this instrument has a known way of lying

+ *
    + *
  1. The capture must contain a scene. A framebuffer enabled at RUNTIME receives the HUD + * pass and not the world pass, so every capture comes back as the clear colour — which reads + * exactly like "the renderer drew nothing". The client must be started with + * {@code -PclientFbo=true}, and the first frame is checked for being more than one flat colour.
  2. + *
  3. The scene must be STATIC. Two captures with no motion between them must be identical. + * If they are not, something in the frame is animating and "frames differ" can no longer mean + * "the camera moved" — the run is inconclusive and says so rather than producing a number.
  4. + *
+ * + *

Why the first run of this class stopped at 4M, and why that was not the render

+ * It delivered with plain {@code /tp} into an arena at {@code Z = 0}. The physics mod cancels, + * silently, any teleport into its reserved shipyard quadrant — {@code chunkX >= 318401 && chunkZ >= + * -1599}, i.e. X ≥ 5,094,416 and Z ≥ -25,584 — while the command still reports success. Every + * rung from 8M up was therefore refused by a mod constant, and the camera never left the previous + * coordinate. The arena now sits at {@code Z = }{@value #ARENA_Z}, below the quadrant's Z edge, and + * the long jump between rungs goes through {@code /artest player far-tp} (vanilla's own + * dimension-change path, which is how a long jump escapes the speed check). The sub-block camera + * steps stay on plain {@code /tp}: they are not long jumps, and they are outside the quadrant. + * + *

Designed to come back NO: if every coordinate shows zero repeats, the render is not the ceiling + * and the cell bound has to be justified by something else or dropped.

+ */ +public class SpikeFarCoordinateRenderJitterTest extends AbstractClientE2ETest { + + /** + * The origin is carried as the CONTROL in the same run: "zero repeats at 16M" means nothing until + * the same instrument has shown zero repeats where no one suspects a quantum. Then today's + * half-cell, the ratified half-cell (16M) and the measured margin (24M). + */ + private static final int[] X_LADDER = {0, 2_000_000, 8_000_000, 16_000_000, 24_000_000}; + + /** + * The arena's Z. The physics mod's reserved quadrant starts at {@code chunkZ >= -1599} + * (Z ≥ -25,584); this sits well below it, so its teleport veto never fires and the only thing + * under test is the coordinate's own magnitude. + */ + private static final int ARENA_Z = -100_000; + + /** Sub-block camera step. Smaller than every quantum in the table, so a quantum shows as repeats. */ + private static final double STEP_BLOCKS = 0.05d; + private static final int STEPS = 12; + /** How many 20-tick waits the frame gets to stop changing on its own before a teleport. */ + private static final int SETTLE_ATTEMPTS = 15; + /** How many (deliver, settle) rounds a rung gets before it is called undeliverable. */ + private static final int DELIVERY_ATTEMPTS = 4; + + private static final int OVERWORLD = 0; + private static final int FLOOR_Y = 140; + private static final int EYE_Y = FLOOR_Y + 1; + + private Path outDir; + private String botName; + + private String exec(String cmd) throws Exception { + return String.join("\n", serverClient().execute(cmd)); + } + + @Test + public void howFarFromTheOriginDoesTheRenderStartToQuantize() throws Exception { + outDir = Paths.get(System.getProperty("forge.test.client.screenshotDir", "build/test-screenshots")) + .toAbsolutePath(); + Files.createDirectories(outDir); + + bot().waitForWorld(); + exec("gamerule sendCommandFeedback false"); + exec("gamerule logAdminCommands false"); + // Freeze everything that could change a pixel for a reason this spike did not cause. + exec("gamerule doDaylightCycle false"); + exec("gamerule doMobSpawning false"); + exec("gamerule doWeatherCycle false"); + exec("weather clear"); + exec("time set 6000"); + + String health = exec("artest player health"); + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("\"player\"\\s*:\\s*\"([^\"]+)\"").matcher(health); + assertTrue("player health must echo the player name: " + health, m.find()); + botName = m.group(1); + + JsonObject fb = bot().setFramebuffer(true); + assertTrue("this client's GL must support the framebuffer capture path: " + fb, + fb.get("supported").getAsBoolean()); + bot().setHudHidden(true); + bot().setRenderDistance(4); + + List report = new ArrayList<>(); + List inconclusive = new ArrayList<>(); + // Per rung: the longest run of byte-identical frames, i.e. the quantum in camera steps. + java.util.Map longestRunByX = new java.util.LinkedHashMap<>(); + + for (int x : X_LADDER) { + // A sealed stone box: the only thing in frame is a wall a few blocks away, so nothing in + // the picture can move on its own (no sky, no sun, no clouds, no weather). + exec("artest chunk forceload " + OVERWORLD + " " + (x >> 4) + " " + (ARENA_Z >> 4)); + // Put the player there FIRST so the chunks around him are live, and build the box only + // then. The first two runs built into unloaded chunks, the player fell into an ocean, and + // every frame was animated water — the controls caught it, but the arrangement had to be + // read off a captured frame to see WHY. FLOOR_Y is well above sea level for the same + // reason: 2M and 16M are both ocean. + deliver(x, FLOOR_Y + 20); + exec("artest fill " + OVERWORLD + " " + (x - 6) + " " + FLOOR_Y + " " + (ARENA_Z - 6) + " " + + (x + 6) + " " + (FLOOR_Y + 5) + " " + (ARENA_Z + 6) + " minecraft:stone"); + exec("artest fill " + OVERWORLD + " " + (x - 5) + " " + (FLOOR_Y + 1) + " " + (ARENA_Z - 5) + + " " + (x + 5) + " " + (FLOOR_Y + 4) + " " + (ARENA_Z + 5) + " minecraft:air"); + // A patterned wall: a flat surface gives a frame whose pixels barely move, and a + // sub-block shift in a flat texture is exactly the change this must be able to see. + exec("artest fill " + OVERWORLD + " " + (x - 5) + " " + (FLOOR_Y + 1) + " " + (ARENA_Z + 5) + + " " + (x + 5) + " " + (FLOOR_Y + 4) + " " + (ARENA_Z + 5) + " minecraft:bookshelf"); + + // ARRANGEMENT CHECK, ON THE AXIS THAT CARRIES THE CONDITION. This used to test posY, and + // posY is right whenever the player stands on ANY floor — so it passed while he was still + // in the previous coordinate's box, and three separate readings were taken of a player who + // was not there. Delivery is RETRIED until the server's own posX says he arrived, and + // abandoned loudly if it never does. + double actualX = deliver(x, EYE_Y); + bot().setLook(0f, 0f); // face +Z, straight at the bookshelf wall + bot().waitTicks(40); + if (!(Math.abs(actualX - (x + 0.5d)) < 2d)) { + inconclusive.add("x=" + x + " the player never arrived (posX=" + actualX + + ", wanted " + (x + 0.5d) + ") - delivery, not the render"); + continue; + } + + BufferedImage first = capture("jitter_" + x + "_ctrl_a"); + if (isFlat(first)) { + inconclusive.add("x=" + x + " capture is one flat colour " + describe(first) + + " - the framebuffer is not receiving the world pass (start with -PclientFbo=true)"); + continue; + } + // SETTLE. The first run said the scene was not static and it was right: chunk streaming, + // lighting propagation and the client's own catch-up keep changing pixels for a while + // after a teleport. Wait for the frame to stop moving ON ITS OWN before asking whether + // MOTION moves it — an unsettled scene answers "the frame changed" to every question. + BufferedImage second = null; + int settleAttempts = 0; + int lastDelta = Integer.MAX_VALUE; + BufferedImage previousSettle = first; + while (settleAttempts < SETTLE_ATTEMPTS) { + settleAttempts++; + bot().waitTicks(20); + BufferedImage now = capture("jitter_" + x + "_settle" + settleAttempts); + lastDelta = differingPixels(previousSettle, now); + previousSettle = now; + if (lastDelta == 0) { + second = now; + break; + } + } + if (second == null) { + inconclusive.add("x=" + x + " the frame never stopped changing on its own after " + + settleAttempts + " attempts (last delta " + lastDelta + "px) - the scene is " + + "not static, so frame differences cannot be attributed to camera motion"); + continue; + } + + List movedOnServer = new ArrayList<>(); + int repeats = 0; + int maxRun = 0; + int run = 0; + BufferedImage previous = second; + for (int step = 1; step <= STEPS; step++) { + double px = x + 0.5d + step * STEP_BLOCKS; + exec("tp " + botName + " " + fmt(px) + " " + EYE_Y + " " + fmt(ARENA_Z + 0.5d)); + bot().waitTicks(8); + // THE MISSING CONTROL. "The frame did not change" and "the player did not move" are + // the same observation until the position is read back. The first version of this + // spike read only the frame and concluded the RENDER quantizes — a conclusion its own + // data could not support. + double serverX = posXOf(exec("artest player health")); + movedOnServer.add(serverX); + BufferedImage now = capture("jitter_" + x + "_step" + step); + if (identical(previous, now)) { + repeats++; + run++; + maxRun = Math.max(maxRun, run); + } else { + run = 0; + } + previous = now; + } + double serverSpan = movedOnServer.isEmpty() ? 0d + : movedOnServer.get(movedOnServer.size() - 1) - movedOnServer.get(0); + int distinctServerPositions = new java.util.HashSet<>(movedOnServer).size(); + double impliedQuantum = maxRun == 0 ? 0d : (maxRun + 1) * STEP_BLOCKS; + report.add("x=" + x + " steps=" + STEPS + + " serverMoved=" + fmt(serverSpan) + "blk/" + distinctServerPositions + "distinct" + + " identicalFrames=" + repeats + + " longestRun=" + maxRun + (maxRun >= STEPS ? " quantum>=" + fmt(STEPS * STEP_BLOCKS) : " quantum~" + fmt(impliedQuantum)) + + " blocks " + + describe(previous)); + // A repeat means "the render did not change". That is only about the RENDER if the camera + // actually moved, so a rung whose stimulus did not land is inconclusive, never a finding. + if (distinctServerPositions < STEPS) { + inconclusive.add("x=" + x + " only " + distinctServerPositions + " of " + STEPS + + " camera steps landed on the server - the stimulus, not the render"); + } else { + longestRunByX.put(x, maxRun); + } + } + + StringBuilder out = new StringBuilder( + "[SPIKE far-coordinate render jitter] step=" + STEP_BLOCKS + " blocks\n"); + for (String line : report) { + out.append(" ").append(line).append('\n'); + } + for (String line : inconclusive) { + out.append(" INCONCLUSIVE ").append(line).append('\n'); + } + System.out.println(out); + writeReport("far-coordinate-render-jitter.txt", out.toString()); + + // The control decides whether anything else here is evidence: at the origin nobody suspects a + // quantum, so if the instrument reports repeats THERE it cannot tell a quantized render from a + // camera that did not move. Asserted first, and separately. + Integer control = longestRunByX.get(0); + assertTrue("the x=0 control produced no usable measurement, so no far rung is evidence:\n" + out, + control != null); + assertTrue("the x=0 control repeated " + control + " frames in a row - a sub-block camera step " + + "does not change the picture even at the origin, so this instrument cannot see the " + + "thing it was built to see:\n" + out, control == 0); + + List quantized = new ArrayList<>(); + for (java.util.Map.Entry e : longestRunByX.entrySet()) { + if (e.getKey() != 0 && e.getValue() > control) { + quantized.add("x=" + e.getKey() + " longestRun=" + e.getValue() + + " (~" + fmt((e.getValue() + 1) * STEP_BLOCKS) + " blocks)"); + } + } + assertTrue("the render quantizes further out than at the origin: " + quantized + "\n" + out, + quantized.isEmpty()); + } + + // ─── helpers ─────────────────────────────────────────────────────────────── + + /** + * Puts the camera at {@code (x + 0.5, y, ARENA_Z + 0.5)} through the long-jump path and returns + * the server's own reading of where he ended up. Retried, because the chunks are force-loaded on + * the SERVER while the client has not received them yet — the first delivery of a rung routinely + * lands in a world the client cannot see. + */ + private double deliver(int x, int y) throws Exception { + double actualX = Double.NaN; + for (int attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt++) { + exec("artest player far-tp " + fmt(x + 0.5d) + " " + y + " " + fmt(ARENA_Z + 0.5d)); + exec("artest server wait " + OVERWORLD + " 60"); + bot().waitTicks(20); + actualX = posXOf(exec("artest player health")); + if (Math.abs(actualX - (x + 0.5d)) < 2d) { + break; + } + } + return actualX; + } + + private BufferedImage capture(String name) throws Exception { + bot().setHudHidden(true); + bot().waitTicks(4); + JsonObject shot = bot().screenshot(name); + assertTrue("screenshot must land on disk: " + shot, shot.get("exists").getAsBoolean()); + Path dst = outDir.resolve(name + ".png"); + Files.copy(Paths.get(shot.get("path").getAsString()), dst, StandardCopyOption.REPLACE_EXISTING); + BufferedImage image = ImageIO.read(new File(dst.toString())); + assertTrue("screenshot must decode: " + dst, image != null); + return image; + } + + private static boolean identical(BufferedImage a, BufferedImage b) { + return differingPixels(a, b) == 0; + } + + private static int differingPixels(BufferedImage a, BufferedImage b) { + if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight()) { + return Integer.MAX_VALUE; + } + int n = 0; + for (int y = 0; y < a.getHeight(); y++) { + for (int x = 0; x < a.getWidth(); x++) { + if ((a.getRGB(x, y) & 0xFFFFFF) != (b.getRGB(x, y) & 0xFFFFFF)) { + n++; + } + } + } + return n; + } + + /** One flat colour = the framebuffer never received the world pass. */ + private static boolean isFlat(BufferedImage img) { + int first = img.getRGB(0, 0) & 0xFFFFFF; + for (int y = 0; y < img.getHeight(); y += 3) { + for (int x = 0; x < img.getWidth(); x += 3) { + if ((img.getRGB(x, y) & 0xFFFFFF) != first) { + return false; + } + } + } + return true; + } + + private static String describe(BufferedImage img) { + return "[" + img.getWidth() + "x" + img.getHeight() + "]"; + } + + /** The server's own reading of where the player is, so the stimulus can be shown to have landed. */ + private static double posXOf(String healthJson) { + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("\"posX\"\\s*:\\s*([-0-9.eE]+)").matcher(healthJson); + return m.find() ? Double.parseDouble(m.group(1)) : Double.NaN; + } + + /** The report is the deliverable, so it also lands on disk and survives a truncated console. */ + private static void writeReport(String name, String text) { + try { + Path dir = Paths.get("build", "spike-reports").toAbsolutePath(); + Files.createDirectories(dir); + Files.write(dir.resolve(name), text.getBytes("UTF-8")); + } catch (Exception e) { + System.out.println("[SPIKE] could not write the report file: " + e); + } + } + + private static String oneLine(String s) { + return s.replace((char) 10, ' ').replace((char) 13, ' ').trim(); + } + + private static String fmt(double v) { + return String.format(java.util.Locale.ROOT, "%.4f", v); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateShipTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateShipTest.java new file mode 100644 index 000000000..5963d1586 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateShipTest.java @@ -0,0 +1,665 @@ +package zmaster587.advancedRocketry.test.client; + +import com.github.stannismod.forge.testing.junit.AbstractClientE2ETest; + +import org.junit.Assume; +import org.junit.Test; +import org.lwjgl.input.Keyboard; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertTrue; + +/** + * SPIKE — does a tier-2 ship survive a far world coordinate the way a bare player does? + * + *

A player walks, stands, collides, holds a sub-block position and is rendered without a quantum + * out to 24M. None of that transfers: a ship's blocks live in the shipyard subspace while its pose + * lives in the world, and the two are bridged by a transform of its own. So the ship is the last + * subject that could still move the ratified half-cell, and this is the leg that measures it.

+ * + *

Why this ASSEMBLES at the coordinate instead of teleporting a ship to it

+ * {@code VSShipExtremeCoordinatesE2ETest} reached extreme Y by rigid-teleporting an assembled + * ship, and left the extreme-|X| leg unautomated for a reason recorded in its own javadoc: after a + * SECOND relocation the physics goes inert — neither a pilot key nor a velocity setpoint moves the + * ship — and the pilot-key path dies after a dismount and re-seat across the map. Those are + * relocation-SEQUENCE findings. Teleporting to |X| would re-run straight into them and produce a red + * that says nothing about the coordinate. + * + *

So the stimulus changes rather than the measurement: the fixture is built, and the ship + * assembled, AT the far coordinate. There is exactly one relocation in the whole leg — the player's, + * through {@code far-tp} — and the ship is never moved at all.

+ * + *

Where the arena sits, and why

+ * {@code Z = }{@value #ARENA_Z}, below the physics mod's reserved quadrant + * ({@code chunkX >= 318401 && chunkZ >= -1599}). Above that Z the quadrant would swallow the arena at + * 16M: the blocks would be shipyard blocks, the player's delivery would be cancelled silently, and + * the leg would measure the reservation instead of the coordinate. + * + *

Acceptance, stated before the run

+ * The {@code x = 0} rung is the control, assembled and flown in the same run by the same commands. + * At every rung: + *
    + *
  1. assembly must produce a VS ship (the ship count rises), and it must LOAD ({@code managed});
  2. + *
  3. the pilot seat must be findable and mountable — crew retention through the far assembly;
  4. + *
  5. a real held vertical-up key must lift the server ship by more than + * {@value #MIN_LIFT_BLOCKS} block;
  6. + *
  7. the CLIENT-rendered rider must track that climb to within {@value #TRACK_TOLERANCE} blocks — + * a transform that has lost precision shows up here as divergence, and nowhere earlier.
  8. + *
+ * + *

Designed to come back NO. A ship that will not assemble, will not load, will not lift or + * whose rider drifts at 16M is a finding against the ratified half-cell, and the number moves.

+ */ +public class SpikeFarCoordinateShipTest extends AbstractClientE2ETest { + + private static final Pattern BUILDER_POS = + Pattern.compile("\"builderPos\":\\[(-?\\d+),(-?\\d+),(-?\\d+)]"); + private static final Pattern POS_Y = Pattern.compile("\"posY\":(-?[0-9.E\\-]+)"); + private static final Pattern COUNT = Pattern.compile("\"count\":(-?\\d+)"); + private static final Pattern DUMMY_ID = Pattern.compile("\"dummyId\":(-?\\d+)"); + private static final Pattern SHIP_ID = Pattern.compile("\"id\"\\s*:\\s*\"([^\"]+)\""); + + /** + * Bounds the ONE nearest-ship lookup this leg makes. The rungs are millions of blocks apart, so a + * radius this size cannot reach a neighbour — and if this rung's own ship is missing, the lookup + * says so instead of describing the other rung's. + */ + private static final int SHIP_LOOKUP_RADIUS = 512; + + private static final Pattern POS_X = Pattern.compile("\"posX\":(-?[0-9.E\\-]+)"); + private static final Pattern POS_Z = Pattern.compile("\"posZ\":(-?[0-9.E\\-]+)"); + + /** One command, then this many samples this many ticks apart, watching for motion to cease. */ + private static final int SURVIVAL_SAMPLES = 40; + private static final int SURVIVAL_SAMPLE_TICKS = 10; + /** Blocks per sample below which the ship counts as no longer being driven. */ + private static final double SURVIVAL_STEP_EPSILON = 0.05d; + + /** The control, then the ratified half-cell. 24M is not carried: one far rung is the question. */ + private static final int[] X_LADDER = {0, 16_000_000}; + + /** Below the reserved quadrant's Z edge (Z ≥ -25,584), so the arena is ordinary world at every X. */ + private static final int ARENA_Z = -100_000; + /** Well above sea level: 16M is ocean, and a fixture built into water is not a fixture. */ + private static final int BASE_Y = 140; + + private static final String VARIANT = "with-pilot-seat"; + private static final double MIN_LIFT_BLOCKS = 1.0d; + private static final double TRACK_TOLERANCE = 3.0d; + private static final double ARRIVAL_TOLERANCE = 1.0d; + private static final int DELIVERY_ATTEMPTS = 4; + /** 5-tick polls the CLIENT gets to agree it is riding the seat the server already mounted it on. */ + private static final int RIDING_ATTEMPTS = 24; + + private String exec(String cmd) throws Exception { + return String.join("\n", serverClient().execute(cmd)); + } + + @Test + public void doesAShipAssembleLoadAndFlyFarFromTheOrigin() throws Exception { + Assume.assumeTrue("needs Valkyrien Skies on the server", serverHasVs()); + + bot().waitForWorld(); + exec("gamerule sendCommandFeedback false"); + exec("gamerule logAdminCommands false"); + exec("gamerule doMobSpawning false"); + exec("gamerule doDaylightCycle false"); + exec("gamerule doWeatherCycle false"); + exec("weather clear"); + // Headless has no player holding a distant ship loaded, and the client is one player in one + // place while two ships exist in this run. + assertTrue(exec("artest vs permaload true").contains("\"ok\":true")); + + Map verdicts = new LinkedHashMap<>(); + // Which ship answered for which rung. Two rungs that report the same id measured one subject + // twice, and two rungs agreeing to four decimals is what that looks like from the outside. + Map shipIds = new LinkedHashMap<>(); + List report = new ArrayList<>(); + List inconclusive = new ArrayList<>(); + StringBuilder out; + + try { + for (int x : X_LADDER) { + int before = count("ship-count-all"); + + String arrangement = arrange(x); + if (arrangement != null) { + inconclusive.add("x=" + x + " " + arrangement); + continue; + } + + String assemble = assembleFixture(x); + if (assemble == null) { + inconclusive.add("x=" + x + " the fixture did not build or did not assemble" + + " (arrangement, not the coordinate)"); + continue; + } + if (!assemble.contains("\"rocketCount\":0")) { + verdicts.put(x, "the build did not route to a SHIP: " + oneLine(assemble)); + continue; + } + + int after = before; + for (int i = 0; i < 40 && after <= before; i++) { + bot().waitTicks(5); + after = count("ship-count-all"); + } + if (after <= before) { + verdicts.put(x, "assembly created no VS ship (count " + before + " -> " + after + ")"); + continue; + } + + // Put the pilot on the ship. This is the ONLY relocation in the leg, and it is the + // player's, not the ship's. + String delivery = deliver(x); + if (delivery != null) { + inconclusive.add("x=" + x + " " + delivery); + continue; + } + + // Capture the ship's IDENTITY once, here — the one moment this lookup is defensible, + // with this rung's ship freshly assembled at this spot. Every later reading goes by + // that id, which has no distance term to be wrong about. + double y0 = Double.NaN; + String shipId = null; + String lastInfo = ""; + for (int i = 0; i < 40 && Double.isNaN(y0); i++) { + bot().waitTicks(5); + lastInfo = exec("artest vs ship-info 0 " + x + " " + BASE_Y + " " + ARENA_Z + + " " + SHIP_LOOKUP_RADIUS); + if (lastInfo.contains("\"managed\":true")) { + y0 = readDouble(lastInfo); + Matcher im = SHIP_ID.matcher(lastInfo); + shipId = im.find() ? im.group(1) : null; + } + } + if (Double.isNaN(y0)) { + verdicts.put(x, "the ship never LOADED with the client present: " + oneLine(lastInfo)); + continue; + } + if (shipId == null) { + verdicts.put(x, "the ship loaded but reported no id, so no later reading can be " + + "attributed to it: " + oneLine(lastInfo)); + continue; + } + if (shipIds.containsValue(shipId)) { + verdicts.put(x, "this rung's ship is the SAME ship a previous rung measured (id " + + shipId + ") - the ladder is measuring one subject twice"); + continue; + } + shipIds.put(x, shipId); + + // NAME the ship. The bare form takes the first loaded pilot seat, and this ladder + // keeps every rung's ship permanently loaded — so at 16M it mounted the pilot onto + // the ORIGIN ship's seat, the client 16M away saw no entity to ride, and the reply + // read exactly like a far-coordinate failure. It was not one. + String mountInfo = exec("artest vs seat-mount 0 near " + x + " " + BASE_Y + " " + + ARENA_Z + " 512"); + if (!mountInfo.contains("\"seatFound\":true")) { + verdicts.put(x, "the pilot seat was not findable: " + oneLine(mountInfo)); + continue; + } + Matcher dm = DUMMY_ID.matcher(mountInfo); + if (!dm.find()) { + verdicts.put(x, "seat-mount reported no dummy id: " + oneLine(mountInfo)); + continue; + } + String mounted = exec("artest player mount-entity " + dm.group(1)); + if (!mounted.contains("\"mounted\":true")) { + verdicts.put(x, "the bot could not mount the seat dummy: " + oneLine(mounted)); + continue; + } + // "mounted":true is the SERVER's word. The climb measures the CLIENT-rendered rider, + // so wait until the CLIENT agrees it is riding — the first run of this leg read the + // rider's posY one tick too early and died on a missing field, which reads exactly + // like a coordinate failure and is not one. + String riding = awaitRiding(Integer.parseInt(dm.group(1))); + if (riding != null) { + verdicts.put(x, riding + " (server said " + oneLine(mounted) + ")"); + continue; + } + + String flight = climbLeg(shipId, y0); + // The seat's own position is a SUBSPACE coordinate — the shipyard is where a ship's + // blocks actually live. Recording it makes the magnitude the ship's own math runs on + // visible in the report, which is the only number that changes if the shipyard moves. + report.add("x=" + x + " ship=" + shipId + " shipY0=" + fmt(y0) + + " subspaceSeatX=" + fmt(field(mountInfo, "seatX")) + + " subspaceSeatZ=" + fmt(field(mountInfo, "seatZ")) + + " " + flight); + verdicts.put(x, flight.startsWith("OK") ? null : flight); + + exec("artest player dismount"); + bot().waitTicks(10); + } + } finally { + // The report is the deliverable and it is worth MOST when the leg died mid-ladder, so it + // is emitted before anything can escape. The first run of this leg threw past its own + // report writer and left nothing on disk to read. + for (Map.Entry e : verdicts.entrySet()) { + if (e.getValue() != null) { + report.add("x=" + e.getKey() + " FAILED " + e.getValue()); + } + } + StringBuilder built = new StringBuilder("[SPIKE far-coordinate VS ship]\n"); + for (String line : report) { + built.append(" ").append(line).append('\n'); + } + for (String line : inconclusive) { + built.append(" INCONCLUSIVE ").append(line).append('\n'); + } + for (int x : X_LADDER) { + if (!verdicts.containsKey(x) && !hasPrefix(inconclusive, "x=" + x + " ")) { + built.append(" NOT REACHED x=").append(x).append('\n'); + } + } + System.out.println(built); + writeReport("far-coordinate-ship.txt", built.toString()); + out = built; + try { + exec("artest player dismount"); + exec("artest vs permaload false"); + } catch (Exception ignored) { + // teardown must not mask the finding + } + } + + // The control is asserted first and separately: a ship that will not fly at the ORIGIN makes + // every far reading meaningless, and that is an instrument failure, not a coordinate ceiling. + assertTrue("the x=0 control produced no measurement at all, so no far rung is evidence:\n" + out, + verdicts.containsKey(0)); + assertTrue("the x=0 control failed - the instrument, not the coordinate: " + verdicts.get(0) + + "\n" + out, verdicts.get(0) == null); + + List failed = new ArrayList<>(); + for (Map.Entry e : verdicts.entrySet()) { + if (e.getKey() != 0 && e.getValue() != null) { + failed.add("x=" + e.getKey() + ": " + e.getValue()); + } + } + assertTrue("a ship does not behave at a far coordinate as it does at the origin: " + failed + + "\n" + out, failed.isEmpty()); + assertTrue("no far rung was measured at all - the leg answered nothing:\n" + out, + verdicts.size() > 1); + } + + /** + * SPIKE — how long does a ONE-SHOT commanded setpoint survive, and does the shipyard's position + * change that? + * + *

The question this exists to settle

+ * Two measurements of this tree disagree. Moving the shipyard to {@code CHUNK_X_START = + * 1,200,000} makes {@code aStillCrewMemberOnAFastClimbingShipKeepsHisCapture} report + * {@code travelled=0.0} on 3 of 3 runs while it is green on 3 of 3 at {@code 320000} — yet the + * ladder above lifts a ship 4.7–5.1 blocks at that same subspace magnitude. Both cannot be + * describing "a ship cannot move out there". + * + *

They stop disagreeing under one hypothesis: the failure is not in DELIVERING a command but + * in its SURVIVAL. The ladder holds a real key, so it re-commands every tick and outlives any + * loss of state; {@code seat-input} writes a setpoint ONCE, into + * {@code TileAdvancedFlightComputer} — and a flight computer tile that is re-created underneath + * the ship loses every live field it holds, {@code velocitySetpoint} included, while persistent + * {@code stationKeeping} survives. A command that is silently dropped a few seconds in reads as + * {@code travelled=0.0}.

+ * + *

Independently, the registration is known to leak in this tree: + * {@code ClaimedChunkCacheController:122} re-registers EVERY tile of a chunk each time the claim + * cache loads it, {@code MixinChunk:48} adds on tile add, and {@code MixinChunk:53} removes only + * when a tile is genuinely removed — so an unload/load cycle leaves the old instance registered + * forever and adds a new one.

+ * + *

What this measures, and what would settle it

+ * One command, then the ship's own position sampled until it stops moving. The number is the + * SURVIVAL WINDOW in ticks. Run at both constants, on a wiped world, at ordinary world + * coordinates so the shipyard's position is the only thing that differs. + *
    + *
  • window shorter at {@code 1,200,000} → the two measurements are reconciled and the + * shipyard move is implicated through the recreation rate;
  • + *
  • window the same → the recreation story is still true but does NOT explain the red, and + * the cause of that red is still unnamed.
  • + *
+ * Prints, never asserts a threshold: there is no defensible number to assert before the first + * pair of readings exists. + */ + @Test + public void howLongDoesAOneShotCommandSurvive() throws Exception { + Assume.assumeTrue("needs Valkyrien Skies on the server", serverHasVs()); + + bot().waitForWorld(); + exec("gamerule sendCommandFeedback false"); + exec("gamerule logAdminCommands false"); + exec("gamerule doMobSpawning false"); + bot().setRenderDistance(4); + assertTrue(exec("artest vs permaload true").contains("\"ok\":true")); + + StringBuilder out = new StringBuilder("[SPIKE one-shot command survival]\n"); + try { + String arrangement = arrange(0); + assertTrue("the arena did not build: " + arrangement, arrangement == null); + String assemble = assembleFixture(0); + assertTrue("the fixture did not assemble", assemble != null); + assertTrue("the build must route to a ship: " + oneLine(assemble), + assemble.contains("\"rocketCount\":0")); + for (int i = 0; i < 40 && count("ship-count-all") < 1; i++) { + bot().waitTicks(5); + } + String delivery = deliver(0); + assertTrue("the pilot was not delivered: " + delivery, delivery == null); + + String shipId = null; + for (int i = 0; i < 40 && shipId == null; i++) { + bot().waitTicks(5); + String info = exec("artest vs ship-info 0 0 " + BASE_Y + " " + ARENA_Z + " " + + SHIP_LOOKUP_RADIUS); + if (info.contains("\"managed\":true")) { + Matcher im = SHIP_ID.matcher(info); + shipId = im.find() ? im.group(1) : null; + } + } + assertTrue("the ship never loaded", shipId != null); + + String mountInfo = exec("artest vs seat-mount 0 near 0 " + BASE_Y + " " + ARENA_Z + " 512"); + assertTrue("no seat: " + oneLine(mountInfo), mountInfo.contains("\"seatFound\":true")); + Matcher dm = DUMMY_ID.matcher(mountInfo); + assertTrue("no dummy id", dm.find()); + assertTrue("could not mount", + exec("artest player mount-entity " + dm.group(1)).contains("\"mounted\":true")); + String riding = awaitRiding(Integer.parseInt(dm.group(1))); + assertTrue("the client never began riding: " + riding, riding == null); + + // ONE command. Forward throttle rather than vertical: horizontal travel has no ceiling to + // be mistaken for a command that stopped surviving. + double[] before = shipXZ(shipId); + String commanded = exec("artest vs seat-input 0 1 0 0 0 0 0"); + out.append(" commanded once: ").append(oneLine(commanded)).append('\n'); + out.append(" subspaceSeat=(").append(fmt(field(mountInfo, "seatX"))).append(',') + .append(fmt(field(mountInfo, "seatZ"))).append(")\n"); + + double lastDist = 0d; + int stoppedAtTick = -1; + int quiet = 0; + for (int sample = 1; sample <= SURVIVAL_SAMPLES; sample++) { + bot().waitTicks(SURVIVAL_SAMPLE_TICKS); + double[] now = shipXZ(shipId); + double dist = Math.hypot(now[0] - before[0], now[1] - before[1]); + double step = dist - lastDist; + out.append(" t=").append(sample * SURVIVAL_SAMPLE_TICKS) + .append(" travelled=").append(fmt(dist)) + .append(" step=").append(fmt(step)).append('\n'); + if (step < SURVIVAL_STEP_EPSILON) { + quiet++; + if (quiet >= 3 && stoppedAtTick < 0 && dist > 0.1d) { + stoppedAtTick = (sample - 2) * SURVIVAL_SAMPLE_TICKS; + } + } else { + quiet = 0; + } + lastDist = dist; + } + out.append(" SURVIVAL WINDOW: ") + .append(stoppedAtTick < 0 + ? "never stopped within " + (SURVIVAL_SAMPLES * SURVIVAL_SAMPLE_TICKS) + + " ticks (total " + fmt(lastDist) + " blocks)" + : stoppedAtTick + " ticks, then motion ceased (total " + fmt(lastDist) + + " blocks)") + .append('\n'); + } finally { + System.out.println(out); + writeReport("one-shot-command-survival.txt", out.toString()); + try { + exec("artest player dismount"); + exec("artest vs permaload false"); + } catch (Exception ignored) { + // teardown must not mask the reading + } + } + } + + /** The ship's world X and Z, by id. */ + private double[] shipXZ(String shipId) { + String last = ""; + for (int i = 0; i < 10; i++) { + try { + last = exec("artest vs ship-info 0 id " + shipId); + Matcher mx = POS_X.matcher(last); + Matcher mz = POS_Z.matcher(last); + if (mx.find() && mz.find()) { + return new double[] {Double.parseDouble(mx.group(1)), + Double.parseDouble(mz.group(1))}; + } + bot().waitTicks(2); + } catch (Exception e) { + throw new AssertionError("ship-info threw: " + e, e); + } + } + throw new AssertionError("ship-info never returned a parseable position; last: " + last); + } + + // ─── the measurement ──────────────────────────────────────────────────────── + + /** + * One controllability measurement where the ship already is: hold the REAL vertical-up key, the + * SERVER ship must climb, and the CLIENT-rendered rider must climb with it. A transform that has + * lost precision at a far coordinate shows up as divergence between those two and nowhere else. + * + * @return {@code "OK ..."} with the numbers, or the reason it failed + */ + /** + * Waits until the CLIENT reports it is riding something, and — if it never does — asks the three + * questions that decide WHICH thing failed, because "the client is not riding" on its own cannot + * tell a coordinate ceiling from an arrangement fault: + *
    + *
  1. where the CLIENT thinks the player is (a client that never arrived explains everything);
  2. + *
  3. what entities the CLIENT can see near him (an empty list means entity tracking never + * delivered the seat dummy — the mount had nothing to bind to);
  4. + *
  5. where the SERVER holds that same dummy (so a client/server split is visible as one).
  6. + *
+ * + * @return {@code null} once the client is riding, else the reason plus that diagnosis + */ + private String awaitRiding(int dummyId) throws Exception { + com.google.gson.JsonObject last = null; + for (int i = 0; i < RIDING_ATTEMPTS; i++) { + bot().waitTicks(5); + last = bot().reportRidingEntity(); + if (last.has("riding") && last.get("riding").getAsBoolean() && last.has("posY")) { + return null; + } + } + String clientState; + String clientEntities; + try { + clientState = String.valueOf(bot().reportState()); + clientEntities = String.valueOf(bot().reportEntities("", 128d)); + } catch (Exception e) { + clientState = "unreadable: " + e; + clientEntities = "unreadable"; + } + return "the CLIENT never began riding the seat after " + (RIDING_ATTEMPTS * 5) + + " ticks (last report: " + last + ")" + + " | client state: " + oneLine(clientState) + + " | client sees near him: " + oneLine(clientEntities) + + " | server holds the dummy at: " + + oneLine(exec("artest entity info 0 " + dummyId)); + } + + private double riderY() throws Exception { + return bot().reportRidingEntity().get("posY").getAsDouble(); + } + + private String climbLeg(String shipId, double yBefore) throws Exception { + double riderYBefore = riderY(); + bot().holdKey(Keyboard.KEY_R); // flightVerticalUp + try { + ClientPoll.until(bot()::waitTicks, + () -> shipY(shipId), + y -> y - yBefore > 1.5, 2, 100); + } finally { + bot().releaseKey(Keyboard.KEY_R); + } + bot().waitTicks(6); + double serverDelta = shipY(shipId) - yBefore; + double riderDelta = riderY() - riderYBefore; + String numbers = "serverLift=" + fmt(serverDelta) + " riderLift=" + fmt(riderDelta) + + " divergence=" + fmt(Math.abs(riderDelta - serverDelta)); + if (!(serverDelta > MIN_LIFT_BLOCKS)) { + // A third witness separates "the seat glue died" from "the ship would not move". + return "the vertical-up key did not lift the ship (" + numbers + "); server player: " + + oneLine(exec("artest player health")); + } + if (Math.abs(riderDelta - serverDelta) >= TRACK_TOLERANCE) { + return "the CLIENT rider did not track the server ship (" + numbers + ")"; + } + return "OK " + numbers; + } + + // ─── arrangement ──────────────────────────────────────────────────────────── + + /** @return {@code null} once the site is loaded and clear, else what is wrong with it */ + private String arrange(int x) throws Exception { + int cx1 = (x - 32) >> 4, cz1 = (ARENA_Z - 32) >> 4; + int cx2 = (x + 32) >> 4, cz2 = (ARENA_Z + 32) >> 4; + String warm = exec("artest chunk warmup 0 " + cx1 + " " + cz1 + " " + cx2 + " " + cz2); + if (!warm.contains("\"ok\":true")) { + return "chunk warmup failed: " + oneLine(warm); + } + // A stone pad at BASE_Y-1 and air above it: 16M is ocean, and the fixture must not be built + // into water or into whatever the generator put there. + exec("artest fill 0 " + (x - 8) + " " + (BASE_Y - 1) + " " + (ARENA_Z - 8) + " " + + (x + 12) + " " + (BASE_Y - 1) + " " + (ARENA_Z + 12) + " minecraft:stone"); + String clear = exec("artest fill 0 " + (x - 8) + " " + BASE_Y + " " + (ARENA_Z - 8) + " " + + (x + 12) + " " + (BASE_Y + 14) + " " + (ARENA_Z + 12) + " minecraft:air"); + if (!clear.contains("\"ok\":true")) { + return "pre-clear failed: " + oneLine(clear); + } + String pad = exec("artest block at 0 " + x + " " + (BASE_Y - 1) + " " + ARENA_Z); + if (!pad.contains("stone")) { + return "the pad is not stone (" + oneLine(pad) + ")"; + } + return null; + } + + /** @return the assemble reply, or {@code null} if the fixture itself never landed */ + private String assembleFixture(int x) throws Exception { + String fixture = exec("artest fixture rocket 0 " + x + " " + BASE_Y + " " + ARENA_Z + + " " + VARIANT); + if (!fixture.contains("\"ok\":true")) { + System.out.println("[SPIKE ship] fixture at x=" + x + " failed: " + oneLine(fixture)); + return null; + } + Matcher bp = BUILDER_POS.matcher(fixture); + if (!bp.find()) { + System.out.println("[SPIKE ship] fixture at x=" + x + " gave no builderPos: " + + oneLine(fixture)); + return null; + } + return exec("artest rocket assemble 0 " + bp.group(1) + " " + bp.group(2) + " " + bp.group(3)); + } + + /** + * Puts the pilot on the ship through the long-jump path, retried: the chunks are loaded on the + * SERVER while the client has not received them yet, and the first delivery of a far rung lands + * in a world the client cannot see. + * + * @return {@code null} once he is there, or a reason string for the INCONCLUSIVE list + */ + private String deliver(int x) throws Exception { + double lastX = Double.NaN; + for (int attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt++) { + exec("artest player far-tp " + fmt(x + 0.5d) + " " + (BASE_Y + 6) + " " + + fmt(ARENA_Z + 0.5d)); + exec("artest server wait 0 40"); + bot().waitTicks(30); + lastX = field(exec("artest player health"), "posX"); + if (Math.abs(lastX - (x + 0.5d)) < ARRIVAL_TOLERANCE) { + return null; + } + } + return "the pilot never arrived (server posX=" + lastX + ", wanted " + (x + 0.5d) + + ") after " + DELIVERY_ATTEMPTS + " deliveries - delivery, not the ship"; + } + + // ─── instruments ──────────────────────────────────────────────────────────── + + /** + * The server ship's {@code posY}, asked BY ID and tolerant of unrelated console lines + * interleaving with the probe's reply — at far coordinates a VS collision mixin can print into + * the same window. + * + *

By id, not by position: a nearest-ship lookup has a distance term to be wrong about, and on + * this ladder — two ships, one of them 16M away — a rung whose own ship had unloaded would + * silently be answered with the OTHER rung's ship. That failure looks like two rungs agreeing to + * four decimals, which is exactly what a clean far-coordinate result also looks like.

+ */ + private double shipY(String shipId) { + String last = ""; + for (int i = 0; i < 10; i++) { + try { + last = exec("artest vs ship-info 0 id " + shipId); + Matcher m = POS_Y.matcher(last); + if (m.find()) { + return Double.parseDouble(m.group(1)); + } + bot().waitTicks(2); + } catch (Exception e) { + throw new AssertionError("ship-info threw: " + e, e); + } + } + throw new AssertionError("ship-info never returned a parseable posY; last reply: " + last); + } + + private int count(String sub) throws Exception { + Matcher m = COUNT.matcher(exec("artest vs " + sub + " 0")); + return m.find() ? Integer.parseInt(m.group(1)) : -1; + } + + private double readDouble(String json) { + Matcher m = POS_Y.matcher(json); + assertTrue("expected a posY in: " + json, m.find()); + return Double.parseDouble(m.group(1)); + } + + private static double field(String json, String key) { + Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*([-0-9.eE]+)").matcher(json); + return m.find() ? Double.parseDouble(m.group(1)) : Double.NaN; + } + + private boolean serverHasVs() throws Exception { + return exec("artest vs available").contains("\"available\":true"); + } + + /** The report is the deliverable, so it also lands on disk and survives a truncated console. */ + private static void writeReport(String name, String text) { + try { + java.nio.file.Path dir = java.nio.file.Paths.get("build", "spike-reports").toAbsolutePath(); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.write(dir.resolve(name), text.getBytes("UTF-8")); + } catch (Exception e) { + System.out.println("[SPIKE] could not write the report file: " + e); + } + } + + private static boolean hasPrefix(List lines, String prefix) { + for (String line : lines) { + if (line.startsWith(prefix)) { + return true; + } + } + return false; + } + + private static String oneLine(String s) { + return s.replace((char) 10, ' ').replace((char) 13, ' ').trim(); + } + + private static String fmt(double v) { + return String.format(Locale.ROOT, "%.4f", v); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeSubBlockPositionGranularityTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeSubBlockPositionGranularityTest.java new file mode 100644 index 000000000..b79236731 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeSubBlockPositionGranularityTest.java @@ -0,0 +1,298 @@ +package zmaster587.advancedRocketry.test.client; + +import com.github.stannismod.forge.testing.junit.AbstractClientE2ETest; +import com.google.gson.JsonObject; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertTrue; + +/** + * SPIKE — does a CONNECTED player's sub-block position survive the client↔server round trip far from + * the origin? + * + *

What this is NOT, and the retraction it carries

+ * This class was first written around the observation "a 0.05-block move lands at 2M and 4M and does + * not land at all at 8M, 12M or 16M", and it went looking for the coordinate at which precision runs + * out. That observation was an artefact of its own delivery. The physics mod cancels, silently, any + * teleport whose destination falls in its reserved shipyard quadrant — {@code chunkX >= 318401 && + * chunkZ >= -1599}, i.e. X ≥ 5,094,416 and Z ≥ -25,584 — and the old arena sat at {@code Z = 0}, + * so every rung from 8M up was refused by a mod constant rather than by any property of the number. + * The command still reported success. Nothing about precision was ever measured. + * + *

So the arena moves to {@code Z = }{@value #ARENA_Z}, below the quadrant's Z edge, where the + * predicate is false at every X, and the long jump between rungs is delivered by + * {@code /artest player far-tp} (vanilla's own dimension-change path, which is how a long jump escapes + * the speed check). The short sub-block steps stay on plain {@code /tp}: they are not long jumps and + * they are outside the quadrant.

+ * + *

The question that is actually left

+ * Two neighbouring facts already exist and neither answers it: + *
    + *
  • {@code SpikeFarCoordinateIntegrityTest} spawns armour stands 0.05 apart out to 28M and reads + * both back exactly — but a SPAWNED entity's position never crosses the wire.
  • + *
  • {@code SpikeFarCoordinatePlayabilityTest} measures a 0.3000 collision stand-off at 16M/20M/24M + * — a sub-block quantity, but one produced by the server's own physics, not asked for.
  • + *
+ * What remains is the round trip: a position ASKED for at a far coordinate, written by the server, + * pushed to the client, and read back from both. If anything in that path narrows to a float, a + * quantum of 1 or 2 blocks at 16M is what it would look like — and it would show here and nowhere else. + * + *

Acceptance, stated before the run

+ * At every rung, for every offset in the ladder {0, 0.05, 0.1, 0.25, 0.5, 1.0} from the same base: + *
    + *
  1. the SERVER's {@code posX} must equal the asked position within + * {@value #SERVER_TOLERANCE} blocks;
  2. + *
  3. the CLIENT's own {@code posX} must agree with it within {@value #CLIENT_TOLERANCE} blocks;
  4. + *
  5. every non-zero offset must read back DISTINCT from the offset-0 base — a quantum would + * collapse the small ones onto it.
  6. + *
+ * {@code x = 0} is carried as the control in the same run, same arena shape, same commands: if the + * control fails, the instrument is broken and no rung is evidence of anything. + * + *

Designed to come back NO. If every offset resolves at 24M exactly as at the origin, the + * wire is not the ceiling either, and "entity doubles degrade past ±2M" has nothing left holding it up + * on any of its three legs.

+ */ +public class SpikeSubBlockPositionGranularityTest extends AbstractClientE2ETest { + + /** The control first, then today's half-cell, the ratified half-cell, and the measured margin. */ + private static final int[] X_LADDER = {0, 2_000_000, 8_000_000, 16_000_000, 24_000_000}; + private static final double[] OFFSETS = {0d, 0.05d, 0.1d, 0.25d, 0.5d, 1.0d}; + + /** + * The arena's Z. The physics mod's reserved quadrant starts at {@code chunkZ >= -1599} + * (Z ≥ -25,584); this sits well below it, so its teleport veto never fires and the only thing + * under test is the coordinate's own magnitude. + */ + private static final int ARENA_Z = -100_000; + + private static final int OVERWORLD = 0; + /** Well above sea level: 2M and 16M are both ocean, and a delivery into water measures the water. */ + private static final int FLOOR_Y = 140; + private static final int STAND_Y = FLOOR_Y + 1; + + private static final double SERVER_TOLERANCE = 0.001d; + private static final double CLIENT_TOLERANCE = 0.05d; + private static final double ARRIVAL_TOLERANCE = 1.0d; + private static final double Y_TOLERANCE = 0.05d; + /** How many (deliver, settle) rounds a rung gets before it is called undeliverable. */ + private static final int DELIVERY_ATTEMPTS = 4; + + private String botName; + + private String exec(String cmd) throws Exception { + return String.join("\n", serverClient().execute(cmd)); + } + + @Test + public void doesASubBlockPositionSurviveTheRoundTripFarFromTheOrigin() throws Exception { + bot().waitForWorld(); + exec("gamerule sendCommandFeedback false"); + exec("gamerule logAdminCommands false"); + exec("gamerule doMobSpawning false"); + exec("gamerule doDaylightCycle false"); + exec("gamerule doWeatherCycle false"); + exec("weather clear"); + bot().setRenderDistance(4); + + String health = exec("artest player health"); + Matcher nm = Pattern.compile("\"player\"\\s*:\\s*\"([^\"]+)\"").matcher(health); + assertTrue("player health must echo the player name: " + health, nm.find()); + botName = nm.group(1); + + List report = new ArrayList<>(); + List inconclusive = new ArrayList<>(); + List broken = new ArrayList<>(); + boolean controlHeld = false; + + // The cheapest competing explanation, asked once: a world border refuses a teleport past it + // while reporting success, and it would produce this whole ladder with no precision story. + report.add("worldborder: " + oneLine(exec("worldborder get"))); + + for (int x : X_LADDER) { + buildFloor(x); + String floorFault = inspectFloor(x); + if (floorFault != null) { + buildFloor(x); // one retry: a fill can lose a race with chunk loading + floorFault = inspectFloor(x); + } + if (floorFault != null) { + inconclusive.add("x=" + x + " the floor did not build - " + floorFault + + " (arrangement, not the coordinate)"); + continue; + } + + String delivery = deliverAndStand(x); + if (delivery != null) { + inconclusive.add("x=" + x + " " + delivery); + continue; + } + + double base = Double.NaN; + List rows = new ArrayList<>(); + List rungFailures = new ArrayList<>(); + for (double offset : OFFSETS) { + double target = x + 0.5d + offset; + exec("tp " + botName + " " + fmt(target) + " " + STAND_Y + " " + fmt(ARENA_Z + 0.5d)); + exec("artest server wait " + OVERWORLD + " 6"); + bot().waitTicks(6); + + double gotServer = serverX(); + double gotClient = clientX(); + if (offset == 0d) { + base = gotServer; + } + double serverErr = Math.abs(gotServer - target); + double clientErr = Math.abs(gotClient - gotServer); + boolean distinct = offset == 0d || Math.abs(gotServer - base) > SERVER_TOLERANCE; + + rows.add("+" + fmt(offset) + " asked " + fmt(target) + + " server " + fmt(gotServer) + " (err " + fmt(serverErr) + ")" + + " client " + fmt(gotClient) + " (delta " + fmt(clientErr) + ")" + + " distinctFromBase=" + distinct); + if (serverErr > SERVER_TOLERANCE) { + rungFailures.add("+" + fmt(offset) + " server missed by " + fmt(serverErr)); + } + if (clientErr > CLIENT_TOLERANCE) { + rungFailures.add("+" + fmt(offset) + " client disagrees by " + fmt(clientErr)); + } + if (!distinct) { + rungFailures.add("+" + fmt(offset) + " collapsed onto the base"); + } + } + + report.add("x=" + x + (rungFailures.isEmpty() ? " OK" : " FAIL " + rungFailures)); + for (String r : rows) { + report.add(" " + r); + } + if (x == 0) { + controlHeld = rungFailures.isEmpty(); + } else if (!rungFailures.isEmpty()) { + broken.add(x + rungFailures.toString()); + } + } + + StringBuilder out = new StringBuilder("[SPIKE sub-block position round trip]\n"); + for (String line : report) { + out.append(" ").append(line).append('\n'); + } + for (String line : inconclusive) { + out.append(" INCONCLUSIVE ").append(line).append('\n'); + } + System.out.println(out); + writeReport("far-coordinate-subblock-roundtrip.txt", out.toString()); + + // The control decides whether anything else in this run is evidence. Asserted FIRST, so a + // broken instrument reports as a broken instrument and not as a coordinate ceiling. + assertTrue("the x=0 control did not resolve its own offset ladder - the instrument is broken, " + + "so no rung here says anything about far coordinates:\n" + out, controlHeld); + assertTrue("a sub-block position was lost at: " + broken + "\n" + out, broken.isEmpty()); + } + + // ─── arrangement ──────────────────────────────────────────────────────────── + + private void buildFloor(int x) throws Exception { + exec("artest chunk forceload " + OVERWORLD + " " + (x >> 4) + " " + (ARENA_Z >> 4)); + exec("artest server wait " + OVERWORLD + " 20"); + exec("artest fill " + OVERWORLD + " " + (x - 4) + " " + FLOOR_Y + " " + (ARENA_Z - 4) + " " + + (x + 4) + " " + FLOOR_Y + " " + (ARENA_Z + 4) + " minecraft:stone"); + exec("artest fill " + OVERWORLD + " " + (x - 4) + " " + STAND_Y + " " + (ARENA_Z - 4) + " " + + (x + 4) + " " + (STAND_Y + 2) + " " + (ARENA_Z + 4) + " minecraft:air"); + } + + /** @return {@code null} if the floor is where it must be, else what is wrong with it */ + private String inspectFloor(int x) throws Exception { + for (int dx : new int[] {0, 1, 2}) { + String at = exec("artest block at " + OVERWORLD + " " + (x + dx) + " " + FLOOR_Y + " " + + ARENA_Z); + if (!at.contains("stone")) { + return "the floor is not stone at x+" + dx + " (" + oneLine(at) + ")"; + } + String above = exec("artest block at " + OVERWORLD + " " + (x + dx) + " " + STAND_Y + " " + + ARENA_Z); + if (!above.contains("minecraft:air")) { + return "the standing space is not air at x+" + dx + " (" + oneLine(above) + ")"; + } + } + return null; + } + + /** + * Delivers the player into the arena and does not return until he is STANDING in it. One delivery + * is not enough: the chunks are force-loaded on the SERVER but the client has not received them + * yet, so client-side physics see air and he falls through the floor. The loop converges rather + * than guessing a settle time, and reports which of the two conditions it never met. + * + * @return {@code null} once he is standing, or a reason string for the INCONCLUSIVE list + */ + private String deliverAndStand(int x) throws Exception { + double lastX = Double.NaN; + double lastY = Double.NaN; + String lastReply = ""; + for (int attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt++) { + lastReply = exec("artest player far-tp " + fmt(x + 0.5d) + " " + STAND_Y + " " + + fmt(ARENA_Z + 0.5d)); + exec("artest server wait " + OVERWORLD + " 40"); + bot().waitTicks(30); + lastX = serverX(); + lastY = serverY(); + if (Math.abs(lastX - (x + 0.5d)) < ARRIVAL_TOLERANCE + && Math.abs(lastY - STAND_Y) < Y_TOLERANCE) { + return null; + } + } + boolean arrived = Math.abs(lastX - (x + 0.5d)) < ARRIVAL_TOLERANCE; + return (arrived + ? "he arrived but would not stand (posY=" + fmt(lastY) + ", floor top " + STAND_Y + ")" + : "the player never arrived (server posX=" + lastX + ", wanted " + (x + 0.5d) + ")") + + " after " + DELIVERY_ATTEMPTS + " deliveries - arrangement, not the coordinate." + + " lastReply=" + oneLine(lastReply); + } + + // ─── instruments ──────────────────────────────────────────────────────────── + + private double serverX() throws Exception { + return field(exec("artest player health"), "posX"); + } + + private double serverY() throws Exception { + return field(exec("artest player health"), "posY"); + } + + /** The CLIENT's own record of where it thinks it is — the far end of the round trip. */ + private double clientX() throws Exception { + JsonObject state = bot().reportState(); + return state.has("playerX") ? state.get("playerX").getAsDouble() : Double.NaN; + } + + private static double field(String json, String key) { + Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*([-0-9.eE]+)").matcher(json); + return m.find() ? Double.parseDouble(m.group(1)) : Double.NaN; + } + + /** The report is the deliverable, so it also lands on disk and survives a truncated console. */ + private static void writeReport(String name, String text) { + try { + java.nio.file.Path dir = java.nio.file.Paths.get("build", "spike-reports").toAbsolutePath(); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.write(dir.resolve(name), text.getBytes("UTF-8")); + } catch (Exception e) { + System.out.println("[SPIKE] could not write the report file: " + e); + } + } + + private static String oneLine(String s) { + return s.replace((char) 10, ' ').replace((char) 13, ' ').trim(); + } + + private static String fmt(double v) { + return String.format(Locale.ROOT, "%.4f", v); + } +} From b1d5670bbd393e59a35d78e5bae47b920d17e5c7 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Fri, 14 Aug 2026 15:52:03 +0300 Subject: [PATCH 12/42] feat: one chart metric for the whole universe layer - state the chart scale (250 m/block) and derive AU, light year, orbit unit - place procedural bodies by their own orbital law, not a second layout - a system that will not fit loses bodies, never scale - separate star spacing from a system's clear space (10 000 AU floor) - tilt an orbit without enlarging it - companions become first-class stars with real orbits and ids - every star of a system lights every world in it - fix groupMax reading the groupMin attribute --- .../api/dimension/solar/StellarBody.java | 149 +++++++- .../render/planet/RenderAsteroidSky.java | 2 +- .../render/planet/RenderPlanetarySky.java | 2 +- .../render/planet/RenderSpaceTravelSky.java | 4 +- .../dimension/DimensionManager.java | 54 ++- .../modules/ModulePlanetSelector.java | 10 +- .../TileHolographicPlanetSelector.java | 10 +- .../universe/BodyEphemeris.java | 28 +- .../universe/ClusteredGalaxyGenerator.java | 159 +++++--- .../universe/GalaxyGenConfig.java | 22 +- .../universe/PlanetDerivation.java | 29 +- .../universe/PlanetRealizer.java | 34 +- .../universe/SystemContent.java | 63 ++-- .../universe/UniverseScale.java | 106 ++++++ .../util/AstronomicalBodyHelper.java | 111 ++++-- .../util/XMLPlanetLoader.java | 31 +- .../test/integration/SystemContentTest.java | 48 +++ .../test/unit/AstronomicalBodyHelperTest.java | 55 ++- .../unit/ClusteredGalaxyGeneratorTest.java | 345 ++++++++++++------ .../test/unit/PlanetDerivationTest.java | 28 +- .../test/unit/PlanetRealizationTest.java | 122 ++++--- .../test/unit/StellarHierarchyTest.java | 188 ++++++++++ .../test/unit/SystemRetinueTest.java | 90 ++++- .../test/unit/UniverseRegistryTest.java | 17 +- 24 files changed, 1313 insertions(+), 394 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/StellarHierarchyTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java b/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java index 87c86e24f..d9b9d385a 100644 --- a/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java +++ b/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java @@ -20,6 +20,29 @@ public class StellarBody { /** {@code M ≈ R^1.25} — the inverse of the main-sequence {@code R ≈ M^0.8}. Exact for Sol. */ private static final double MAIN_SEQUENCE_MASS_EXPONENT = 1.25d; + /** + * How far a companion orbits its primary when nothing has said — in the same distance units a + * planet's orbit is in (100 = 1 AU), so this is 0.05 AU: a close pair, the kind that reads as two + * suns in one sky rather than as a second star elsewhere in the system. + * + *

The field this replaces was an ANGLE with the same default of 5, applied to the sky as a + * tilt. An angle cannot say where a companion is — only how far off the primary it looks from one + * particular world — so nothing could place it, light a planet by it, or let it move.

+ */ + public static final int DEFAULT_COMPANION_ORBIT = 5; + + /** + * Solar-map units per AU — the multiplier {@code DimensionProperties.getSpacePosition} lays a + * planet out with (100 map units per 100 distance units, i.e. per AU). Stated here so a star and + * a planet at the same orbital distance land at the same place on one map. + */ + private static final double PLANET_MAP_UNITS_PER_AU = 100d; + + /** Sentinel for {@link #baseTheta}: nobody has stated one, so binding picks a phase. */ + private static final double THETA_UNSTATED = Double.NaN; + /** The golden angle, in radians — how unstated companion phases are spread. */ + private static final double GOLDEN_ANGLE = 2.399963229728653d; + public List subStars; int numPlanets; int discoveredPlanets; @@ -29,7 +52,10 @@ public class StellarBody { private float mass = MASS_UNSET; String name; short posX, posZ; - float starSeperation; + /** This star's orbit about its primary, in distance units (100 = 1 AU). Zero for a primary. */ + private int orbitalDistance; + /** Its angle on that orbit at tick zero, in radians; {@link #THETA_UNSTATED} until bound. */ + private double baseTheta = THETA_UNSTATED; StellarBody parentStar; private int temperature; private HashMap planets; @@ -40,7 +66,7 @@ public StellarBody() { planets = new HashMap<>(); size = 1f; subStars = new LinkedList<>(); - starSeperation = 5f; + orbitalDistance = DEFAULT_COMPANION_ORBIT; isBlackHole = false; diskAngle = 70; } @@ -49,14 +75,31 @@ public List getSubStars() { return subStars; } + /** + * Bind {@code star} as a companion of this one. + * + *

The companion keeps its own identity. This used to overwrite the companion's id with + * the primary's, which made a companion unaddressable: a planet binds to its star by a flat + * {@code starId}, so with both stars answering the same number there was no value that could mean + * "I orbit the companion" — no companion could own a world, and neither a wide binary nor a + * three-star hierarchy was expressible however well the storage nested. Minting the id is the star + * registry's job, because the id space is the registry's; this method only states the + * relationship.

+ */ public void addSubStar(StellarBody star) { if (star.name == null) star.setName(name + "-" + (subStars.size() + 1)); - star.setId(this.id); + if (Double.isNaN(star.baseTheta)) + star.baseTheta = subStars.size() * GOLDEN_ANGLE; subStars.add(star); star.parentStar = this; } + /** This star's primary, or {@code null} when it is the one its system is named for. */ + public StellarBody getParentStar() { + return parentStar; + } + public boolean isBlackHole() { return isBlackHole; } @@ -69,13 +112,68 @@ public int getDisplayRadius() { return (int) (100 * size); } - //Returns the distance between the star and sub stars - public float getStarSeparation() { - return starSeperation; + /** + * How far this star orbits its primary, in distance units (100 = 1 AU) — the same field a planet + * carries, meaning the same thing. Zero, and meaningless, for a star that is nobody's companion. + */ + public int getOrbitalDistance() { + return orbitalDistance; + } + + public void setOrbitalDistance(int distanceUnits) { + this.orbitalDistance = Math.max(0, distanceUnits); + } + + /** This star's angle on its orbit at tick zero, in radians. */ + public double getBaseTheta() { + return Double.isNaN(baseTheta) ? 0d : baseTheta; } - public void setStarSeparation(float seperation) { - this.starSeperation = seperation; + public void setBaseTheta(double radians) { + this.baseTheta = radians; + } + + /** + * This star's offset from the one its SYSTEM is named for, in AU, as a two-element + * {@code (x, z)} pair at tick zero — the barycentric geometry a companion needs to be placed, + * lit by, or measured against. + * + *

Zero for a primary, and composed up the chain for a companion of a companion, so a + * three-star hierarchy is the same arithmetic as a pair rather than a special case.

+ */ + public double[] offsetFromSystemAu() { + if (parentStar == null) { + return new double[] {0d, 0d}; + } + double[] parent = parentStar.offsetFromSystemAu(); + double a = orbitalDistance / 100d; // 100 distance units to the AU + double theta = getBaseTheta(); + return new double[] {parent[0] + a * Math.cos(theta), parent[1] + a * Math.sin(theta)}; + } + + /** The distance between two stars of one system, in AU, at tick zero. */ + public double separationAuFrom(StellarBody other) { + if (other == null) { + return 0d; + } + double[] a = offsetFromSystemAu(); + double[] b = other.offsetFromSystemAu(); + return Math.hypot(a[0] - b[0], a[1] - b[1]); + } + + /** + * How far apart this star and its primary look, in DEGREES, seen from a world orbiting the + * primary at {@code observerOrbitalDistance}. + * + *

A real angle from a real distance, so a close pair reads as two suns almost together and a + * wide one puts its companion somewhere else in the sky entirely — which is the difference the + * old constant tilt could not express.

+ */ + public float apparentSeparationDegrees(int observerOrbitalDistance) { + if (parentStar == null || orbitalDistance <= 0 || observerOrbitalDistance <= 0) { + return 0f; + } + return (float) Math.toDegrees(Math.atan2(orbitalDistance, observerOrbitalDistance)); } public float getSize() { @@ -142,11 +240,14 @@ public IDimensionProperties removePlanet(IDimensionProperties planet) { } /** - * @return the number of planets orbiting this star + * @return the number of planets orbiting THIS star + * + *

A companion answers for its own worlds, not for its primary's. It used to delegate upward + * while {@link #addPlanet} filled the companion's own map, so a companion with planets reported + * its primary's count and a companion with none reported a number that was not zero — the same + * object disagreeing with itself about what it holds.

*/ public int getNumPlanets() { - if (parentStar != null) - return parentStar.getNumPlanets(); return numPlanets; } @@ -263,7 +364,8 @@ public void writeToNBT(NBTTagCompound nbt) { if (mass > MASS_UNSET) { nbt.setFloat("mass", mass); } - nbt.setFloat("seperation", starSeperation); + nbt.setInteger("companionOrbit", orbitalDistance); + nbt.setDouble("companionTheta", getBaseTheta()); nbt.setBoolean("isBlackHole", isBlackHole); nbt.setFloat("diskAngle", diskAngle); @@ -293,8 +395,9 @@ public void readFromNBT(NBTTagCompound nbt) { mass = nbt.hasKey("mass") ? nbt.getFloat("mass") : MASS_UNSET; - if (nbt.hasKey("seperation")) - starSeperation = nbt.getFloat("seperation"); + if (nbt.hasKey("companionOrbit")) + orbitalDistance = nbt.getInteger("companionOrbit"); + baseTheta = nbt.hasKey("companionTheta") ? nbt.getDouble("companionTheta") : THETA_UNSTATED; subStars.clear(); if (nbt.hasKey("subStars")) { @@ -309,8 +412,22 @@ public void readFromNBT(NBTTagCompound nbt) { } } + /** + * Where this star stands on the legacy solar map: the system's own star at the origin, and a + * companion offset by its orbit about whatever it orbits. + * + *

It used to answer an empty position for every star, so the space layer placed every + * companion of every system at the same point — the one place a star of a binary certainly is + * not. The offset uses the same distance multiplier a planet's does, so a companion and a planet + * at the same orbital distance land at the same place on the map, which is the whole reason the + * two carry the same field in the same unit.

+ */ public SpacePosition getSpacePosition() { - //TODO - return new SpacePosition(); + SpacePosition position = new SpacePosition(); + position.star = this; + double[] offset = offsetFromSystemAu(); + position.x = offset[0] * PLANET_MAP_UNITS_PER_AU; + position.z = offset[1] * PLANET_MAP_UNITS_PER_AU; + return position; } } diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java index 8d062ea8a..27c74bd0a 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java @@ -659,7 +659,7 @@ protected void drawStarAndSubStars(BufferBuilder buffer, StellarBody sun, Dimens GL11.glRotatef(phaseInc, 0, 1, 0); GL11.glPushMatrix(); - GL11.glRotatef(subStar.getStarSeparation() * AstronomicalBodyHelper.getBodySizeMultiplier(solarOrbitalDistance), 1, 0, 0); + GL11.glRotatef(subStar.apparentSeparationDegrees(solarOrbitalDistance), 1, 0, 0); float[] color = subStar.getColor(); drawStar(buffer, subStar, properties, solarOrbitalDistance, subStar.getSize(), new Vec3d(color[0], color[1], color[2]), multiplier); diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java index 47d28b170..458714777 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java @@ -1146,7 +1146,7 @@ protected void drawStarAndSubStars(BufferBuilder buffer, StellarBody sun, Dimens GL11.glRotatef(phaseInc, 0, 1, 0); GL11.glPushMatrix(); - GL11.glRotatef(subStar.getStarSeparation() * AstronomicalBodyHelper.getBodySizeMultiplier(solarOrbitalDistance), 1, 0, 0); + GL11.glRotatef(subStar.apparentSeparationDegrees(solarOrbitalDistance), 1, 0, 0); float[] color = subStar.getColor(); drawStar(buffer, subStar, properties, solarOrbitalDistance, subStar.getSize(), new Vec3d(color[0], color[1], color[2]), multiplier); GL11.glPopMatrix(); diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderSpaceTravelSky.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderSpaceTravelSky.java index ed3d2e159..2c3ed2f00 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderSpaceTravelSky.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderSpaceTravelSky.java @@ -649,7 +649,9 @@ private void buildSolarSystem(SpacePosition playerPosition) { phase += phaseInc; //Get substar separation for placement from the orbital distance of the substars - SpacePosition subStarSpacePosition = mainStarPos.getFromSpherical(40 * subStar.getStarSeparation(), theta); + SpacePosition subStarSpacePosition = + mainStarPos.getFromSpherical(40d * subStar.getOrbitalDistance(), + subStar.getBaseTheta()); renderStar(subStar, subStarSpacePosition, playerPosition); } diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java index 7504c9ea7..290e631b0 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java @@ -597,24 +597,68 @@ public StellarBody getStar(int id) { } /** - * @return a list of star ids + * @return the ids of the SYSTEMS — one per star that is nobody's companion + * + *

Companions are addressable through {@link #getStar(int)} but are not systems: they are drawn, + * saved, synced and placed as part of the primary they orbit. A consumer that walked every + * registered star instead would draw a binary twice on the map, write it twice to XML and give + * its companion a galactic address of its own.

*/ public Set getStarIds() { - return starList.keySet(); + Set ids = new HashSet<>(); + for (Entry e : starList.entrySet()) { + if (e.getValue() != null && e.getValue().getParentStar() == null) { + ids.add(e.getKey()); + } + } + return ids; } + /** The SYSTEMS — see {@link #getStarIds()}. */ public Collection getStars() { - - return starList.values(); + List primaries = new ArrayList<>(); + for (StellarBody star : starList.values()) { + if (star != null && star.getParentStar() == null) { + primaries.add(star); + } + } + return primaries; } /** - * Adds a star to the handler + * Adds a star to the handler, together with every companion under it. + * + *

A companion is a star like any other and gets an id of its own here, because the id space is + * this registry's to hand out and a companion that is not in {@code starList} cannot be resolved + * by {@link #getStar(int)} — which is how a planet finds the star it orbits. Without that, a + * companion could be described but never orbited: the hierarchy existed in storage and nowhere + * else.

+ * + *

An id already in use by a DIFFERENT star is replaced rather than honoured; a companion that + * already holds its own id (a reload, a re-registration) keeps it, so ids survive a save.

* * @param star star to add */ public void addStar(StellarBody star) { + if (star == null) { + return; + } starList.put(star.getId(), star); + addCompanionsOf(star); + } + + private void addCompanionsOf(StellarBody primary) { + for (StellarBody companion : primary.getSubStars()) { + if (companion == null) { + continue; + } + StellarBody holder = starList.get(companion.getId()); + if (holder != null && holder != companion) { + companion.setId(getNextFreeStarId()); + } + starList.put(companion.getId(), companion); + addCompanionsOf(companion); + } } /** diff --git a/src/main/java/zmaster587/advancedRocketry/inventory/modules/ModulePlanetSelector.java b/src/main/java/zmaster587/advancedRocketry/inventory/modules/ModulePlanetSelector.java index b4106eef7..45d3bf747 100644 --- a/src/main/java/zmaster587/advancedRocketry/inventory/modules/ModulePlanetSelector.java +++ b/src/main/java/zmaster587/advancedRocketry/inventory/modules/ModulePlanetSelector.java @@ -209,8 +209,10 @@ private void renderGalaxyMap(IGalaxy galaxy, int posX, int posY, float distanceZ displaySize = (int) (planetSizeMultiplier * star2.getDisplayRadius()); int deltaX, deltaY; - deltaX = (int) ((int) (star2.getStarSeparation() * MathHelper.cos(phase) * 0.5*distanceZoomMultiplier)); - deltaY = (int) ((int) (star2.getStarSeparation() * MathHelper.sin(phase) * 0.5*distanceZoomMultiplier)); + deltaX = (int) (star2.getOrbitalDistance() + * Math.cos(star2.getBaseTheta()) * 0.5 * distanceZoomMultiplier); + deltaY = (int) (star2.getOrbitalDistance() + * Math.sin(star2.getBaseTheta()) * 0.5 * distanceZoomMultiplier); planetList.add(button = new ModuleButton( offsetX + deltaX, @@ -270,8 +272,8 @@ private void renderStarSystem(StellarBody star, int posX, int posY, float distan displaySize = (int) (planetSizeMultiplier * star2.getDisplayRadius()); int deltaX, deltaY; - deltaX = (int) (star2.getStarSeparation() * MathHelper.cos(phase) * 0.5); - deltaY = (int) (star2.getStarSeparation() * MathHelper.sin(phase) * 0.5); + deltaX = (int) (star2.getOrbitalDistance() * Math.cos(star2.getBaseTheta()) * 0.5); + deltaY = (int) (star2.getOrbitalDistance() * Math.sin(star2.getBaseTheta()) * 0.5); planetList.add(button = new ModuleButton( offsetX + deltaX, offsetY + deltaY, diff --git a/src/main/java/zmaster587/advancedRocketry/tile/station/TileHolographicPlanetSelector.java b/src/main/java/zmaster587/advancedRocketry/tile/station/TileHolographicPlanetSelector.java index 9798a5a9a..7bc96084c 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/station/TileHolographicPlanetSelector.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/station/TileHolographicPlanetSelector.java @@ -127,8 +127,10 @@ public void update() { float phase = 0; for (EntityUIStar entity : starEntities) { double deltaX, deltaY; - deltaX = (entity.getStarProperties().getStarSeparation() * MathHelper.cos(phase) * 0.05); - deltaY = (entity.getStarProperties().getStarSeparation() * MathHelper.sin(phase) * 0.05); + deltaX = entity.getStarProperties().getOrbitalDistance() + * Math.cos(entity.getStarProperties().getBaseTheta()) * 0.05; + deltaY = entity.getStarProperties().getOrbitalDistance() + * Math.sin(entity.getStarProperties().getBaseTheta()) * 0.05; entity.setPosition(this.pos.getX() + .5 + getInterpHologramSize() * deltaX, this.pos.getY() + 1, this.pos.getZ() + .5 + getInterpHologramSize() * deltaY); entity.setScale(getInterpHologramSize() * entity.getStarProperties().getSize()); @@ -287,8 +289,8 @@ private void rebuildSystem() { for (StellarBody body : starList) { double deltaX, deltaY; - deltaX = (body.getStarSeparation() * MathHelper.cos(phase) * 0.05); - deltaY = (body.getStarSeparation() * MathHelper.sin(phase) * 0.05); + deltaX = body.getOrbitalDistance() * Math.cos(body.getBaseTheta()) * 0.05; + deltaY = body.getOrbitalDistance() * Math.sin(body.getBaseTheta()) * 0.05; EntityUIStar entity = new EntityUIStar(world, body, count++, this, this.pos.getX() + .5 + deltaX, this.pos.getY() + 1, this.pos.getZ() + .5 + deltaY); this.getWorld().spawnEntity(entity); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/BodyEphemeris.java b/src/main/java/zmaster587/advancedRocketry/universe/BodyEphemeris.java index ed0a6af33..196eae559 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/BodyEphemeris.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/BodyEphemeris.java @@ -58,8 +58,15 @@ public static BodyEphemeris fixed(long dx, long dy, long dz) { } /** - * An orbit about whatever this body is bound to: {@code (d·cos θ, d·sin φ, d·sin θ)} in units of - * {@code unitBlocks}, with {@code θ = (2π·(t mod P)/P + baseTheta) · (retrograde ? −1 : +1)}. + * An orbit about whatever this body is bound to: {@code (d·cos φ·cos θ, d·sin φ, d·cos φ·sin θ)} in + * units of {@code unitBlocks}, with {@code θ = (2π·(t mod P)/P + baseTheta) · (retrograde ? −1 : +1)}. + * + *

The inclination tilts the orbit; it does not enlarge it. The law used to read + * {@code (d·cos θ, d·sin φ, d·sin θ)}, whose length is {@code d·√(1 + sin²φ)} — so an inclined body + * stood further from its primary than its own orbital distance said, by up to 41 % at the steepest + * authored angle. Every number derived from that distance (insolation, temperature, period) said + * one thing while the flight said another, which is exactly the split this frame exists to close. + * With the cosine factor the offset's length is {@code d} at every inclination.

* *

The retrograde sign multiplies the SUM, not the time term alone — that is the shipped law and * a body's NAME is derived through it, so changing the grouping would move every retrograde body's @@ -82,6 +89,18 @@ public double distUnits() { return distUnits; } + /** + * The base angle this law was built with, in RADIANS — where the body stands at tick zero, before + * any time has passed. + * + *

Read it rather than recovering an angle from where the body's cell ended up: a cell is coarse, + * so the recovered angle is the drawn one rounded to whatever the cell grid could express, and two + * consumers rounding it separately put the same body in two places.

+ */ + public double baseTheta() { + return baseTheta; + } + public boolean isStatic() { return unitBlocks == 0L || !(periodTicks > 0d) || Double.isInfinite(periodTicks) || distUnits == 0d; @@ -94,10 +113,11 @@ public BlockDelta offsetAt(long tick) { } double theta = thetaAt(tick); double phi = Math.toRadians(phiDegrees); + double inPlane = distUnits * Math.cos(phi); return BlockDelta.of( - Math.round(distUnits * Math.cos(theta) * unitBlocks), + Math.round(inPlane * Math.cos(theta) * unitBlocks), Math.round(distUnits * Math.sin(phi) * unitBlocks), - Math.round(distUnits * Math.sin(theta) * unitBlocks)); + Math.round(inPlane * Math.sin(theta) * unitBlocks)); } /** The orbital angle (radians) at {@code tick}. */ diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 1a8d6da8f..018608d76 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -12,6 +12,7 @@ import zmaster587.advancedRocketry.api.Constants; import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; import zmaster587.advancedRocketry.space.AbsolutePos; +import zmaster587.advancedRocketry.space.BlockDelta; import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; @@ -106,9 +107,9 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { private static final double NUDGE_ANGLE = 2.399963229728653d; // the golden angle, in radians /** How many relocations a body gets before its system is declared full. */ private static final int NUDGE_ATTEMPTS = 96; - /** Neighbourhood margin (cells) kept clear of the super-cell boundary. */ + /** Neighbourhood margin (cells) kept clear of the seat's own clear space. */ private static final int NEIGHBOURHOOD_MARGIN_CELLS = 2; - /** Thin-disk half-thickness as a fraction of the orbit radius (bodies keep honest 3D Y — A#1a e1). */ + /** Thin-disk half-thickness as a fraction of the orbit radius (bodies keep honest 3D Y). */ private static final double PROC_DISK_FRACTION = 0.1d; private final GalaxyGenConfig config; @@ -196,11 +197,12 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { // A star does not move inside its own system: its frame IS the system's anchor. bodies.add(SystemBody.fixedAt(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, starId)); - // Bodies orbit at cell-scale radii: min 1 cell out (never the anchor cell), max = the bounded - // neighbourhood radius. The anchor sits in the middle band of its super-cell (>= 3s/8 from every - // face), so a radius <= 3s/8 - margin keeps every body inside the anchor's super-cell — member-cell - // attribution by floorDiv stays exact. (The per-body box clamp below covers the tiny-spacing floor.) + // A body sits where its ORBIT puts it — one law, one constant, the same one an authored system + // uses. What the neighbourhood decides is not how far a body goes but how many bodies there is + // room for: orbits are drawn inside a bracket that already fits, and a system that would run + // past its own clear space loses BODIES rather than being squashed to fit. long s = config.minSpacing; + double outerBound = maxNamedOrbitUnits(s); // AT MOST ONE REAL BODY PER CELL, moons excepted. The draw picks each body's angle and radius // independently, so two of them CAN land on the same cell — and two real bodies in one cell are @@ -215,39 +217,43 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { int outermostOrbit = 0; int innermostGiantOrbit = 0; for (int i = 0; i < count; i++) { - // The ORBIT is drawn first and the cell radius follows it, rather than the other way round: - // a body's physics is derived from its orbit, so letting the placement pick the radius would - // make every world's climate a function of the layout arithmetic. + // The ORBIT is drawn first and the cell follows from it, rather than the other way round: + // a body's physics is derived from its orbit, so letting the placement pick the distance + // would make every world's climate a function of the layout arithmetic. + // + // The orbit is drawn across the STAR'S OWN zone, and a body that lands outside the room + // this system has is DROPPED. Narrowing the bracket instead would have kept the body and + // moved it inward, which is the one thing this whole seam exists to prevent: a world's + // distance is its star's business, and a system squeezed by its neighbours holds fewer + // worlds rather than the same worlds at the wrong distances. int orbit = PlanetDerivation.orbitalDistanceOf(seed, cell, i, count, star); - GalacticCoord addr = placeBody(seed, cell, i, orbit, star, s, taken); - if (addr == null) { + if (orbit > outerBound) { + continue; // outside this system's clear space — a bound of the layout, not a failure + } + Seat seat = seatBody(seed, cell, i, orbit, star, s, taken); + if (seat == null) { continue; // this system's neighbourhood is full — a bound of the layout, not a failure } // Planet or giant is not a roll of its own: it falls out of the body's derived physics, // which is what makes the zoning (rock inside, giants past the snow line) emerge instead // of being authored. Kept here rather than at realization because the nav list, the sky // and the descent trigger all read the kind long before anyone lands. - BodyProfile profile = PlanetDerivation.derive(seed, cell, addr, 0, star, false, orbit); + BodyProfile profile = PlanetDerivation.derive(seed, cell, seat.cell, 0, star, false, orbit); // THE ORBIT LIVES IN THE FRAME, not in the body's own offset — the same shape an authored // system uses (SystemContent: a planet sits at its frame origin and the FRAME goes round // the star). Built with the convenience constructor, a procedural planet got // CellFrame.staticAt(...) and a FIXED offset, so it stood still relative to its star // forever while its own moons orbited it, and the identical system authored in XML moved. - double theta = PlanetRealizer.angleOf(cell, addr); - double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY - * AstronomicalBodyHelper.getOrbitalPeriod(orbit, star.getMass()); - CellFrame bodyFrame = CellFrame.of(AbsolutePos.ofCellName(cell.cellCentre()), - BodyEphemeris.orbit(orbit, theta, 0d, false, periodTicks, - SystemContent.ORBIT_UNIT_BLOCKS)); + CellFrame bodyFrame = CellFrame.of(AbsolutePos.ofCellName(cell.cellCentre()), seat.law); // Procedural bodies have no realized dimension yet — a descent (Layer 2) realizes one. - bodies.add(new SystemBody(addr, bodyFrame, BodyEphemeris.STATIC, profile.kind(), + bodies.add(new SystemBody(seat.cell, bodyFrame, BodyEphemeris.STATIC, profile.kind(), Constants.INVALID_PLANET, starId, orbit)); outermostOrbit = Math.max(outermostOrbit, orbit); if (profile.kind() == SystemBodyKind.GAS_GIANT && (innermostGiantOrbit == 0 || orbit < innermostGiantOrbit)) { innermostGiantOrbit = orbit; } - addMoons(bodies, seed, cell, addr, bodyFrame, orbit, star, starId, profile); + addMoons(bodies, seed, cell, seat.cell, bodyFrame, orbit, star, starId, profile); } // An inner belt is DERIVED from a giant and never rolled: it is material a giant's resonances @@ -259,9 +265,14 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { // The outer belt is MANDATORY on every system — the Kuiper analogue, and the reason every system // is worth arriving in: it is a gravity-well-free mining site that needs no landing, so a ship // that drifts into any system at all has something to work. - int outerBelt = (int) Math.max(outermostOrbit * OUTER_BELT_FACTOR, + // + // It is the one body allowed to sit past the drawn bracket, because it is defined as being + // beyond the outermost world; what it may NOT pass is the system's own clear space, and there + // it is bounded like everything else rather than being quietly dropped. + double outerBelt = Math.max(outermostOrbit * OUTER_BELT_FACTOR, PlanetDerivation.innerOrbit(star) * 2d); - addBelt(bodies, seed, cell, outerBelt, star, s, starId, taken, count + 2); + addBelt(bodies, seed, cell, (int) Math.min(outerBelt, outerBound), star, s, starId, taken, + count + 2); return bodies; } @@ -276,55 +287,80 @@ public static int retinueSize(long seed, GalacticCoord anchor) { return Math.max(1, Math.min(MAX_PROC_PLANETS, n)); } + /** + * How far this system's NAMED bodies may reach from its star, in orbital-distance units: the + * declared clear space around a seat, or as much of it as this spacing can actually give. + */ + private static double maxNamedOrbitUnits(long s) { + long reachCells = Math.max(1L, UniverseScale.seatMarginCells(s) - NEIGHBOURHOOD_MARGIN_CELLS); + return Math.min(UniverseScale.MAX_NAMED_ORBIT_UNITS, + UniverseScale.orbitUnitsForCells(reachCells)); + } + /** * Claim a free cell for a body orbiting at {@code orbit}, or {@code null} when the neighbourhood has * no room left. * - *

The first choice puts the body at the cell radius its orbit maps to, at a drawn angle. If that - * cell is already spoken for, the body is walked around the ring by the golden angle — which keeps - * its radius, and therefore keeps the system's cell layout in the same order as its orbits — and - * only then allowed to drift outward. A body that still finds nothing is dropped: a neighbourhood - * holds what it holds, and inventing a second occupant for a cell is the one outcome that is worse - * than a smaller system.

+ *

The cell is READ OFF the body's own orbital law at the naming instant, not computed by a second + * arithmetic beside it: the name a body carries and the frame its cell rides are then the same + * statement evaluated once, and cannot drift apart when either is retuned. That is exactly how an + * authored body is named, which is what makes one orbital distance mean one distance in both + * families.

+ * + *

If the first choice is already spoken for, the body is walked around its ring by the golden + * angle — a relocation costs a body its ANGLE and never its distance, so no world's climate is + * disturbed by the layout arithmetic and the orbital order survives. A body that still finds nothing + * is dropped: a neighbourhood holds what it holds, and inventing a second occupant for a cell is the + * one outcome that is worse than a smaller system.

*/ - private static GalacticCoord placeBody(long seed, GalacticCoord anchor, int index, int orbit, - StellarBody star, long s, Set taken) { - long maxRadiusCells = Math.max(1L, 3L * s / 8L - NEIGHBOURHOOD_MARGIN_CELLS); - double maxRadiusBlocks = (double) maxRadiusCells * GalacticCoord.CELL; - double minRadiusBlocks = GalacticCoord.CELL; + private static Seat seatBody(long seed, GalacticCoord anchor, int index, int orbit, + StellarBody star, long s, Set taken) { double baseAngle = CellHash.norm(CellHash.ofBody(seed, anchor, index, SALT_BODYANG)) * 2d * Math.PI; - double baseRadius = minRadiusBlocks + PlanetDerivation.orbitFraction(orbit, star) - * Math.max(0d, maxRadiusBlocks - minRadiusBlocks); - double heightFraction = CellHash.norm(CellHash.ofBody(seed, anchor, index, SALT_BODYY)) - 0.5d; + // Out-of-plane displacement lives in the LAW as an inclination, so a body's height above the + // disk is part of where it IS at every tick rather than a one-off nudge applied to its name. + double sinPhi = (CellHash.norm(CellHash.ofBody(seed, anchor, index, SALT_BODYY)) - 0.5d) + * PROC_DISK_FRACTION; + double phiDegrees = Math.toDegrees(Math.asin(sinPhi)); + double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY + * AstronomicalBodyHelper.getOrbitalPeriod(orbit, star.getMass()); for (int attempt = 0; attempt < NUDGE_ATTEMPTS; attempt++) { - double angle = baseAngle + attempt * NUDGE_ANGLE; - // Radius is held for a full turn of the ring before it is allowed to grow, so a relocation - // costs the body its angle long before it costs it its place in the orbital order. - double radius = Math.min(maxRadiusBlocks, baseRadius * (1d + 0.06d * (attempt / 16))); - long lx = (long) (radius * Math.cos(angle)); - long lz = (long) (radius * Math.sin(angle)); - long ly = (long) (heightFraction * radius * PROC_DISK_FRACTION); - // The body's address is its OWN cell's centre (zone content sits near the cell centre — A#1a), - // box-clamped into the anchor's super-cell so member attribution stays exact at ANY minSpacing - // (at tiny spacings the floor above can otherwise push a body across the super-cell face). - GalacticCoord addr = clampIntoSuperCell(anchor.plusLocal(lx, ly, lz).cellCentre(), anchor, s); + BodyEphemeris law = BodyEphemeris.orbit(orbit, baseAngle + attempt * NUDGE_ANGLE, + phiDegrees, false, periodTicks, AstronomicalBodyHelper.BLOCKS_PER_ORBIT_UNIT); + BlockDelta at0 = law.offsetAt(SystemContent.NAME_TICK); + // The body's address is its OWN cell's centre (zone content sits near the cell centre), + // box-clamped into the anchor's super-cell so member attribution stays exact at ANY + // spacing — at tiny spacings a whole orbit can otherwise reach across the super-cell face. + GalacticCoord addr = clampIntoSuperCell( + anchor.plusLocal(at0.dx(), at0.dy(), at0.dz()).cellCentre(), anchor, s); if (taken.add(addr.cellKey())) { - return addr; + return new Seat(addr, law); } } return null; } + /** A body's claimed cell together with the orbital law that put it there — one statement, not two. */ + private static final class Seat { + final GalacticCoord cell; + final BodyEphemeris law; + + Seat(GalacticCoord cell, BodyEphemeris law) { + this.cell = cell; + this.law = law; + } + } + /** Append an asteroid belt at {@code orbit}, if the neighbourhood still has a cell for one. */ private static void addBelt(List bodies, long seed, GalacticCoord anchor, int orbit, StellarBody star, long s, int starId, Set taken, int index) { int clamped = Math.max(1, orbit); - GalacticCoord addr = placeBody(seed, anchor, index, clamped, star, s, taken); - if (addr != null) { - // A belt is centred on the star it rings, so as a whole it does not travel round it. - bodies.add(SystemBody.fixedAt(addr, SystemBodyKind.ASTEROID_BELT, Constants.INVALID_PLANET, - starId, clamped)); + Seat seat = seatBody(seed, anchor, index, clamped, star, s, taken); + if (seat != null) { + // A belt is centred on the star it rings, so as a whole it does not travel round it. Its + // cell is a marker on the ring; the ring itself does not go anywhere. + bodies.add(SystemBody.fixedAt(seat.cell, SystemBodyKind.ASTEROID_BELT, + Constants.INVALID_PLANET, starId, clamped)); } } @@ -432,11 +468,18 @@ private Optional systemForSuperCell(long seed, long supX, long supY, return Optional.empty(); } long s = config.minSpacing; - // Seat the anchor in the middle band of the super-cell ([3s/8, 5s/8)): every face stays >= 3s/8 - // cells away, so a body neighbourhood of radius <= 3s/8 - margin can never cross into the - // neighbouring super-cell (A#1a attribution guarantee). - long band = Math.max(1L, s / 4L); - long base = 3L * s / 8L; + // Seat the anchor anywhere in its cube except a declared margin at the faces. That margin is + // the system's own CLEAR SPACE, not a fraction of the cube: it is what guarantees two stars + // never stand closer than the separation floor, and what keeps one system's named bodies from + // reaching into the next cube (so member-cell attribution by floorDiv stays exact). + // + // It used to be the middle quarter per axis, which confined the seat to 1.6 % of the cube's + // volume — a lattice of tight clumps with guaranteed-empty walls between them, visible in any + // rendered star field. The margin now costs a couple of percent per face instead, because it + // is sized by what a system actually needs rather than by the distance to the next star. + long margin = UniverseScale.seatMarginCells(s); + long band = Math.max(1L, s - 2L * margin); + long base = margin; long ox = base + Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_OX), band); long oy = base + Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_OY), band); long oz = base + Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_OZ), band); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index a2ffdaae4..09cbc2783 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -17,13 +17,18 @@ public final class GalaxyGenConfig { /** - * Default super-cell edge in cells. Sized so a system's per-body-cell NEIGHBOURHOOD (planets at their - * own cells, ~1M blocks per orbit-unit — universe-model §2 amendment A#1a) fits inside half a - * super-cell: neighbourhoods of two neighbouring systems can never interleave. Deliberately a FIXED - * constant, never derived from the planet catalog — {@code minSpacing} partitions procedural space, and - * deriving it from XML content would silently relocate the whole procedural galaxy on any catalog edit. + * Default super-cell edge in cells: the mean distance between neighbouring stars, converted through + * the chart metric by {@link UniverseScale#DEFAULT_SPACING_CELLS}. + * + *

It no longer decides how big a system is. A system's extent follows its outermost orbit and is + * bounded by the separation floor, so this number moves the STARS apart and nothing else — raising + * it does not inflate a single planet's orbit, and lowering it does not squash one.

+ * + *

Deliberately a FIXED constant, never derived from the planet catalog: it partitions procedural + * space, and deriving it from XML content would silently relocate the whole procedural galaxy on any + * catalog edit.

*/ - public static final int DEFAULT_MIN_SPACING = 512; + public static final int DEFAULT_MIN_SPACING = UniverseScale.DEFAULT_SPACING_CELLS; /** A weighted star archetype: a temperature (drives colour) and a size range. */ public static final class StarType { @@ -42,7 +47,10 @@ public StarType(int temperature, float minSize, float maxSize, int weight) { /** Per-super-cell occupancy probability inside a galaxy (before the void mask). */ public final double density; - /** Super-cell edge in cells: at most one system per {@code minSpacing}-cube. Minimum system spacing. */ + /** + * Super-cell edge in cells: at most one system per {@code minSpacing}-cube, i.e. how far apart + * stars stand. It bounds no orbit — see {@link #DEFAULT_MIN_SPACING}. + */ public final int minSpacing; /** Blob field resolution in super-cells — the size of a galaxy cluster. */ public final int clusterScale; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java index cc9e6d245..920e14d73 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java @@ -185,8 +185,13 @@ public static int referenceDistance(StellarBody star) { * *

Each body owns a SLOT of the logarithmic range and is jittered inside it by less than half a * slot, so the draw is irregular but the ordering is not: body {@code i} is always inside body - * {@code i+1}. That is what lets the placement map an orbit onto a cell radius monotonically, and it - * is why two bodies of one system cannot swap places when a tuning constant moves.

+ * {@code i+1}. That is why two bodies of one system cannot swap places when a tuning constant + * moves.

+ * + *

The zone is the STAR'S, and nothing else's. How much room the system has where it + * sits is not an input here: a body that will not fit is the caller's to drop, because a distance + * bent to fit a neighbourhood is a world whose climate, insolation and year all describe a place + * it is not standing.

*/ public static int orbitalDistanceOf(long seed, GalacticCoord anchor, int index, int count, StellarBody star) { @@ -210,22 +215,10 @@ public static double outerOrbit(StellarBody star) { return Math.max(innerOrbit(star) * 1.5d, referenceDistance(star) * OUTER_ORBIT_FACTOR); } - /** - * Where {@code orbitalDistance} sits in this star's zone, as a fraction in {@code [0,1]} on a - * LOGARITHMIC scale — the inverse of the orbital draw. - * - *

This is what lets the galactic placement map an orbit onto a cell radius: the two layouts then - * agree by construction, so a body that is third from its star is also third out from the anchor - * cell, and neither can be re-tuned without the other following.

- */ - public static double orbitFraction(int orbitalDistance, StellarBody star) { - double lo = innerOrbit(star); - double hi = outerOrbit(star); - if (!(hi > lo)) { - return 0d; - } - return clamp(Math.log(Math.max(lo, orbitalDistance) / lo) / Math.log(hi / lo), 0d, 1d); - } + // orbitFraction — where an orbit sat in its star's zone, as a fraction — lived here to map an + // orbit onto a cell radius, which is a job the placement no longer has: a body's cell is read off + // its own orbital law, so there is nothing left to normalise against. Removed rather than left + // callerless, because the next caller would be re-introducing the second scale it existed to serve. /** The bare (no-atmosphere) equilibrium temperature at a distance — the zoning reading. */ public static int bareTemperature(StellarBody star, int orbitalDistance) { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java index 2ee7c8983..04bea31ce 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java @@ -144,7 +144,7 @@ public static int realize(MinecraftServer server, GalacticCoord bodyCell) { BodyProfile profile = PlanetDerivation.derive(registry.worldSeed(), anchor, target.name(), variant, star, target.kind() == SystemBodyKind.MOON, target.orbitalDistance()); - DimensionProperties props = materialize(dimId, profile, star, anchor, target, parentBody); + DimensionProperties props = materialize(dimId, profile, star, target, parentBody); if (!DimensionManager.getInstance().registerDim(props, true)) { LOGGER.error("[UNIVERSE] dimension {} was already registered while realizing {}", dimId, @@ -169,8 +169,7 @@ public static int realize(MinecraftServer server, GalacticCoord bodyCell) { * {@code Random}. */ private static DimensionProperties materialize(int dimId, BodyProfile profile, StellarBody star, - GalacticCoord anchor, SystemBody body, - SystemBody parentBody) { + SystemBody body, SystemBody parentBody) { DimensionProperties props = new DimensionProperties(dimId); props.setName(star.getName() + " " + dimId); props.setStar(star); @@ -194,9 +193,13 @@ private static DimensionProperties materialize(int dimId, BodyProfile profile, S body.name().cellKey(), parentBody.dimId()); } } - // The orbital angle is READ OFF the body's cell rather than drawn again, so the planet the sky - // shows and the planet the orbital elements describe are in the same place. - props.baseOrbitTheta = angleOf(anchor, body.name()); + // The orbital angle is taken from the body's own law, so the planet the sky shows and the + // planet the orbital elements describe are in the same place. A planet's angle lives in the + // FRAME its cell rides; a moon's lives in its own offset law, because a moon shares its + // parent's frame and going through that would hand it its parent's angle instead of its own. + BodyEphemeris ownLaw = body.kind() == SystemBodyKind.MOON + ? body.offsetLaw() : body.frame().law(); + props.baseOrbitTheta = ownLaw.baseTheta(); props.orbitTheta = props.baseOrbitTheta; props.setAtmosphereDensityDirect(profile.pressure()); @@ -272,20 +275,7 @@ private static int rotationalPeriodOf(BodyProfile profile, StellarBody star) { return profile.rotationalPeriodTicks(); } - /** The angle of a body's cell about its system's anchor, in radians. */ - /** - * A body's orbital angle, read off its cell rather than drawn again — so the sky, the orbital - * elements and the frame a body rides all put it in the same place. Shared with - * {@link ClusteredGalaxyGenerator}, which must build the frame from the same angle this writes - * into {@code baseOrbitTheta}; a second copy of this arithmetic would let the two drift. - */ - static double angleOf(GalacticCoord anchor, GalacticCoord bodyCell) { - long dx = bodyCell.sectorX() - anchor.sectorX(); - long dz = bodyCell.sectorZ() - anchor.sectorZ(); - if (dx == 0L && dz == 0L) { - return 0d; - } - double theta = Math.atan2((double) dz, (double) dx); - return theta < 0d ? theta + 2d * Math.PI : theta; - } + // angleOf — recovering a body's orbital angle from where its cell ended up — is gone: the angle is + // now carried by the body's own law, which is what the cell was derived FROM. Recovering it was + // only ever an approximation of the drawn value, accurate to whatever the cell grid could express. } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java index 61ebb0eda..879f8326c 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java @@ -25,25 +25,29 @@ * Derives the addressable {@link SystemBody} content of an AUTHORED system (a catalogued {@link StellarBody}) * from its planets/moons (universe-model.md §2 amendment A#1a + §4). A system is an anchored * NEIGHBOURHOOD of cells: the star sits at the anchor cell's centre; every planet/belt gets its own cell - * at a sector offset scaled from its orbital position ({@link #ORBIT_UNIT_BLOCKS ~1M blocks per orbit-unit}, - * {@code tunable}), snapped to that cell's centre (zone content sits near the cell centre); moons stay LOCAL + * at a sector offset scaled from its orbital position ({@link #ORBIT_UNIT_BLOCKS blocks per orbit-unit}), + * snapped to that cell's centre (zone content sits near the cell centre); moons stay LOCAL * inside their parent planet's cell. Inter-body space is cells of void. * *

A body's cell is its durable NAME, derived once at {@link #NAME_TICK} and thereafter recorded. Where * that cell IS stays a function of time: each body cell carries a {@link CellFrame} whose origin is its * primary's position, so the neighbourhood rides the body it belongs to and a moon orbits inside it.

* - *

The neighbourhood is BOUNDED: every body cell is clamped (with a WARN) into the anchor's - * {@code minSpacing}-cube super-cell, {@link #BOX_MARGIN_CELLS} cells clear of its faces — the load-time - * guard that keeps two systems' neighbourhoods from interleaving, whatever an XML author wrote for - * {@code orbitalDistance} (its cap is {@code Integer.MAX_VALUE}).

+ *

The neighbourhood is BOUNDED: every body cell is clamped (with a WARN) into the system's declared + * clear space around its anchor — the load-time guard that keeps two systems' neighbourhoods from + * interleaving, whatever an XML author wrote for {@code orbitalDistance} (its cap is + * {@code Integer.MAX_VALUE}).

* *

Pure DATA — a walkable realization is Layer 2. Scale constants are {@code tunable}.

*/ public final class SystemContent { - /** Blocks per unit of {@code DimensionProperties} orbital distance (A#1a: ~1M blocks per orbit-unit). */ - static final long ORBIT_UNIT_BLOCKS = 1_000_000L; + /** + * Blocks per unit of {@code DimensionProperties} orbital distance — the chart metric's own + * conversion, shared with the procedural generator so that one orbital distance means one distance + * in both families. + */ + static final long ORBIT_UNIT_BLOCKS = AstronomicalBodyHelper.BLOCKS_PER_ORBIT_UNIT; /** Blocks per unit of a moon's (parent-relative) orbital distance — moons cluster near their planet. */ static final long MOON_UNIT_BLOCKS = 200L; /** Cells kept clear of the super-cell faces when clamping a body cell into its system's box. */ @@ -299,15 +303,15 @@ private static void auditOneRealBodyPerCell(List bodies, int starId) private static GalacticCoord clampIntoBox(GalacticCoord bodyCell, GalacticCoord anchor, int minSpacingCells, int dimId) { long s = Math.max(1, minSpacingCells); - long margin = (s > 2L * BOX_MARGIN_CELLS) ? BOX_MARGIN_CELLS : 0L; - long cx = clampAxis(bodyCell.sectorX(), anchor.sectorX(), s, margin); - long cy = clampAxis(bodyCell.sectorY(), anchor.sectorY(), s, margin); - long cz = clampAxis(bodyCell.sectorZ(), anchor.sectorZ(), s, margin); + long reach = reachCells(s); + long cx = clampAxis(bodyCell.sectorX(), anchor.sectorX(), reach); + long cy = clampAxis(bodyCell.sectorY(), anchor.sectorY(), reach); + long cz = clampAxis(bodyCell.sectorZ(), anchor.sectorZ(), reach); if ((cx != bodyCell.sectorX() || cy != bodyCell.sectorY() || cz != bodyCell.sectorZ()) && REPORTED.add("clamp:" + dimId + ':' + bodyCell.cellKey())) { - LOGGER.warn("orbit of dim {} exceeds the system neighbourhood bound (minSpacing {} cells); " - + "clamping its cell from ({},{},{}) into the anchor's super-cell", - dimId, s, bodyCell.sectorX(), bodyCell.sectorY(), bodyCell.sectorZ()); + LOGGER.warn("orbit of dim {} reaches past its system's clear space ({} cells at a spacing " + + "of {}); clamping its cell from ({},{},{}) back inside it", + dimId, reach, s, bodyCell.sectorX(), bodyCell.sectorY(), bodyCell.sectorZ()); } return GalacticCoord.ofSectorLocal(cx, cy, cz, 0L, 0L, 0L); } @@ -322,16 +326,30 @@ public static boolean withinBoxOf(GalacticCoord cell, GalacticCoord anchor, int if (cell == null || anchor == null) { return false; } - long s = Math.max(1, minSpacingCells); - long margin = (s > 2L * BOX_MARGIN_CELLS) ? BOX_MARGIN_CELLS : 0L; - long reach = Math.max(0L, s / 2L - margin); + long reach = reachCells(Math.max(1, minSpacingCells)); return Math.abs(cell.sectorX() - anchor.sectorX()) <= reach && Math.abs(cell.sectorY() - anchor.sectorY()) <= reach && Math.abs(cell.sectorZ() - anchor.sectorZ()) <= reach; } /** - * The per-axis bound: {@code half - margin} cells either side OF THE ANCHOR. + * How far from its anchor a body of this system may be NAMED, in cells: the system's declared clear + * space, or as much of it as this spacing can give. + * + *

It used to be half the spacing outright, which was the same number while a system's extent was + * defined as a fraction of the distance to the next star. Once stars stand a real distance apart, + * half of that is several thousand times more room than a system has any business occupying, and an + * authored orbit could be named right up against the neighbouring star. The bound that matters is + * the system's own clear space, and it is the same one the procedural generator seats against.

+ */ + private static long reachCells(long s) { + long margin = (s > 2L * BOX_MARGIN_CELLS) ? BOX_MARGIN_CELLS : 0L; + return Math.min(Math.max(0L, s / 2L - margin), + Math.max(0L, UniverseScale.SEAT_MARGIN_CELLS - margin)); + } + + /** + * The per-axis bound: {@code reach} cells either side OF THE ANCHOR. * *

This used to snap to the GRID super-cell containing the anchor — * {@code [floorDiv(anchor,s)*s + margin, … + s-1-margin]} — which is a different box, and for the @@ -345,12 +363,7 @@ public static boolean withinBoxOf(GalacticCoord cell, GalacticCoord anchor, int * Centring the box on the anchor is also what this class's javadoc and * {@code ClusteredGalaxyGenerator} ("minSpacing/2 - margin") always claimed it did.

*/ - private static long clampAxis(long sector, long anchorSector, long s, long margin) { - long half = s / 2L; - long reach = half - margin; - if (reach < 0L) { - reach = 0L; - } + private static long clampAxis(long sector, long anchorSector, long reach) { long lo = anchorSector - reach; long hi = anchorSector + reach; if (sector < lo) { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java new file mode 100644 index 000000000..8b2c765d4 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java @@ -0,0 +1,106 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; + +/** + * How big the universe layer's furniture is, in one place: how far apart stars stand, how much room a + * system is allowed to occupy, and how those two turn into cells. + * + *

Every number here is stated as a PHYSICAL length and converted, never written as a cell count. + * A cell count is a reading of {@link GalacticCoord#CELL}, and that constant may move; a light year + * may not. Stating the physics and deriving the cells is what keeps the star field looking the same + * after the cell edge is retuned.

+ * + *

The two lengths, and why they are separate

+ *
    + *
  • Star separation — the edge of the cube that holds at most one system. It decides how + * far a flight between two stars is, and nothing else.
  • + *
  • The separation floor — the guaranteed clear space around a system. It decides how much + * room a system's bodies have and how close two unrelated systems may ever be seen to stand.
  • + *
+ * + *

These used to be one number: a system's extent was defined as a fraction of the + * interstellar step, which truncated systems at a few AU, filled half the gap to the next star with + * one system's neighbourhood, and forced the orbit scale to shrink to compensate. Separating them is + * what lets a system be as big as its outermost orbit while the sky still reads as a sky.

+ * + *

A near-pair of lattice seats is NOT a binary — it is two unrelated systems with two names, two + * frames and no gravitational relation between them. The floor is therefore set comfortably wider + * than any binary the model describes, so multiplicity is something the generator states inside ONE + * system rather than something the lattice fakes by accident.

+ */ +public final class UniverseScale { + + /** + * Mean distance between neighbouring star seats, in light years. Real stellar neighbourhoods run + * 4–5 light years between neighbours; the lattice is stratified rather than Poisson, so the + * mean neighbour distance it produces comes out somewhat above this edge. + */ + public static final double MEAN_STAR_SEPARATION_LY = 4.23d; + + /** + * The guaranteed clear space around a system, in AU: two stars never stand closer than this, + * however the lattice falls. Four times the widest binary the star model describes, so a lattice + * near-pair can never be mistaken for one. + * + *

It bounds SEATS. Each system's named bodies then stay inside half of it (see + * {@link #MAX_NAMED_ORBIT_UNITS}), which is what makes two neighbourhoods unable to overlap.

+ */ + public static final double SEPARATION_FLOOR_AU = 10_000d; + + /** + * How far a system's NAMED bodies may reach from their star, in orbital-distance units — half the + * separation floor, which is exactly what makes two systems' neighbourhoods unable to overlap. + * + *

It is a bound, not a size. An ordinary system ends at its outermost orbit (a few tens of AU); + * this is the wall a system that would grow past its own clear space runs into, and the rule when + * it does is that the system loses BODIES, never scale.

+ * + *

Diffuse, nameless matter — a comet cloud — is not bound by it and may reach past a + * neighbour's, exactly as real ones nearly touch: attribution reads names, not matter.

+ */ + public static final int MAX_NAMED_ORBIT_UNITS = + (int) Math.min(Integer.MAX_VALUE, + Math.round(SEPARATION_FLOOR_AU / 2d * AstronomicalBodyHelper.DISTANCE_UNITS_PER_AU)); + + /** The same reach, in cells: the margin a system's seat keeps clear of its cube's faces. */ + public static final long SEAT_MARGIN_CELLS = cellsForOrbitUnits(MAX_NAMED_ORBIT_UNITS); + + /** + * Default edge of the cube that holds at most one system, in cells. Derived from + * {@link #MEAN_STAR_SEPARATION_LY}; a balance knob, overridable from the universe generator's + * configuration, and never a contract. + */ + public static final int DEFAULT_SPACING_CELLS = (int) Math.min(Integer.MAX_VALUE, + Math.max(1L, Math.round(MEAN_STAR_SEPARATION_LY + * AstronomicalBodyHelper.BLOCKS_PER_LIGHT_YEAR / (double) GalacticCoord.CELL))); + + private UniverseScale() { + } + + /** How many cells an orbital distance spans. Rounded up: a reach must not come out short. */ + public static long cellsForOrbitUnits(double orbitUnits) { + double blocks = Math.max(0d, orbitUnits) * AstronomicalBodyHelper.BLOCKS_PER_ORBIT_UNIT; + return (long) Math.ceil(blocks / (double) GalacticCoord.CELL); + } + + /** The largest orbital distance that fits inside {@code cells} cells of a system's star. */ + public static double orbitUnitsForCells(long cells) { + return Math.max(0d, cells) * (double) GalacticCoord.CELL + / AstronomicalBodyHelper.BLOCKS_PER_ORBIT_UNIT; + } + + /** + * The clear margin a system of edge {@code spacingCells} actually gets: the declared one, or as + * much of it as a cube that small can give. + * + *

A cube smaller than twice the floor cannot honour the floor — that is a degenerate galaxy, + * not an error, and it is what a test or a pack asking for a compact universe gets. What may + * never happen is a margin so large that no seat is left, so it stops one short of half the cube.

+ */ + public static long seatMarginCells(long spacingCells) { + long half = Math.max(0L, (spacingCells - 1L) / 2L); + return Math.min(SEAT_MARGIN_CELLS, half); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java index c852e3945..44dffdfbd 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java +++ b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java @@ -25,6 +25,44 @@ public class AstronomicalBodyHelper { public static final int KELVIN_PER_STAR_TEMPERATURE_UNIT = 58; /** Solar radii in one astronomical unit — carries a star's size into the distance frame. */ public static final int SOLAR_RADII_PER_AU = 215; + + // ─── The CHART metric ────────────────────────────────────────────────────── + // How a physical length becomes a number of blocks in the chart — the space bodies are placed, + // sized and separated in. It is NOT the metric of a world anyone walks on: a loaded world is + // metres per block, and the two are never added. A length that crosses the boundary crosses it + // at materialization (a descent shell), nowhere else. + // + // Everything below is DERIVED from the two physical facts and the scale, so no consumer may + // write its own conversion: one edit to the scale moves the whole chart consistently. + + /** Metres in one chart block — the scale the whole universe layer is drawn at. */ + public static final int METRES_PER_CHART_BLOCK = 250; + /** Metres in one astronomical unit (IAU 2012). */ + public static final double METRES_PER_AU = 1.495_978_707e11d; + /** Metres in one Julian light year. */ + public static final double METRES_PER_LIGHT_YEAR = 9.460_730_472_580_8e15d; + + /** Chart blocks in one astronomical unit. */ + public static final long BLOCKS_PER_AU = + Math.round(METRES_PER_AU / METRES_PER_CHART_BLOCK); + /** Chart blocks in one light year. */ + public static final long BLOCKS_PER_LIGHT_YEAR = + Math.round(METRES_PER_LIGHT_YEAR / METRES_PER_CHART_BLOCK); + /** + * Chart blocks per unit of {@code orbitalDistance} — the ONE law that turns an orbit into a + * place, for authored and procedural systems alike. + * + *

It used to be a literal million blocks per unit, six times too small, because a system's + * extent was defined as a fraction of the distance to the next star and the orbit scale was + * shrunk until systems fit. Extent now follows the outermost orbit, so the scale can be what the + * metric says it is and one orbit unit means one distance everywhere.

+ */ + public static final long BLOCKS_PER_ORBIT_UNIT = BLOCKS_PER_AU / DISTANCE_UNITS_PER_AU; + + /** Earth's equatorial radius in metres — the unit a body's {@code radius} is stated in. */ + public static final double EARTH_RADIUS_METRES = 6_378_137d; + /** Earth's radius in chart blocks: what one unit of a body's radius is worth on the chart. */ + public static final double EARTH_RADIUS_BLOCKS = EARTH_RADIUS_METRES / METRES_PER_CHART_BLOCK; /** * Earth's albedo — the reflectivity a world is assumed to have when its type has not stated one. * It was hard-coded into the temperature formula with a comment saying it could not easily be @@ -234,32 +272,30 @@ public static double getStellarBrightness(StellarBody star, int orbitalDistance) return MIN_BRIGHTNESS; } float planetaryOrbitalRadius = orbitalDistance / (float) DISTANCE_UNITS_PER_AU; - // EVERY star that shines on this world contributes, and what ADDS is the FLUX each one - // delivers here — not their luminosities. Radiant power from mutually incoherent sources - // superposes linearly, so E = sum of L_i / d_i², with each star's own distance under its own - // luminosity. Summing luminosities first and dividing once is the same number only while all - // the stars are equidistant from the planet. + // EVERY star of the system shines on this world, and what ADDS is the FLUX each one delivers + // here — not their luminosities. Radiant power from mutually incoherent sources superposes + // linearly, so E = sum of L_i / d_i², with each star's own distance under its own luminosity. + // Summing luminosities first and dividing once is the same number only while all the stars + // are equidistant from the planet. // - // Today they are, by construction rather than by physics: a companion's separation is stored - // as an ANGLE in the sky (StellarBody.getStarSeparation), so there is no distance to give it, - // and every companion is fed the primary's. That is exact for the close binaries the model can - // actually describe, and it is why this sums flux terms rather than luminosities — when a - // companion gains a real orbital radius, only the argument below changes. + // The walk starts at the system's ROOT, not at the star the planet is bound to, so a world of + // a companion is lit by the primary as well — an S-type planet is a planet in a binary, not a + // planet with one sun that happens to have a bright neighbour. Each star's distance is the + // separation between it and the planet's own star, combined with the planet's orbit: the + // planet's direction round its star is not known here, so the two lengths compose in + // quadrature. That is exact when they are perpendicular, and correct in both limits — a close + // companion converges to the planet's own orbital radius, a distant one to the separation. // - // This replaces a walk over the companions whose only effect was to clear a boolean: any - // ordinary companion turned the accretion-disc dimming OFF, after which the brightness came - // from the BLACK HOLE's own size and temperature at full strength, and the companion itself - // never contributed a photon. + // This replaces feeding every companion the PRIMARY's distance, which was exact only for the + // close binaries the old angle-valued separation could describe: a companion twenty AU out + // warmed a world as though it were sitting one AU away. //Returns ratio compared to a planet at 1 AU for Sol, because the other values in AR are normalized, //and this works fairly well for hooking into with other mod's solar panels & such - double brightness = fluxOf(star, planetaryOrbitalRadius); - Iterable companions = star.getSubStars(); - if (companions != null) { - for (StellarBody companion : companions) { - if (companion != null) { - brightness += fluxOf(companion, planetaryOrbitalRadius); - } - } + double brightness = 0d; + for (StellarBody member : systemOf(star)) { + double separationAu = member == star ? 0d : member.separationAuFrom(star); + brightness += fluxOf(member, + (float) Math.hypot(planetaryOrbitalRadius, separationAu)); } // Guarantee: never return 0, NaN, or Infinity @@ -269,6 +305,37 @@ public static double getStellarBrightness(StellarBody star, int orbitalDistance) return brightness; } + /** + * Every star of the system {@code member} belongs to — its root primary and every companion + * under it, at any depth. A three-star hierarchy is walked the same way a pair is, so nothing + * downstream needs a case for one. + */ + public static java.util.List systemOf(StellarBody member) { + java.util.List all = new java.util.ArrayList<>(); + if (member == null) { + return all; + } + StellarBody root = member; + while (root.getParentStar() != null) { + root = root.getParentStar(); + } + collectStars(root, all); + return all; + } + + private static void collectStars(StellarBody star, java.util.List into) { + if (star == null || into.contains(star)) { + return; // a cycle in an authored hierarchy must not hang the light calculation + } + into.add(star); + Iterable companions = star.getSubStars(); + if (companions != null) { + for (StellarBody companion : companions) { + collectStars(companion, into); + } + } + } + /** * The flux one star delivers at {@code orbitalRadiusAu}, relative to Sol at 1 AU: * {@code size² · (T/Sol)⁴ / r²} — Stefan-Boltzmann over the inverse square, both in solar units. diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index 6f3255c1c..88899f8c6 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -100,7 +100,8 @@ public class XMLPlanetLoader { private static final String ATTR_SIZE = "size"; private static final String ATTR_NUMPLANETS = "numPlanets"; private static final String ATTR_NUMGASPLANETS = "numGasGiants"; - private static final String ATTR_SEPERATION = "separation"; + private static final String ATTR_COMPANION_ORBIT = "orbitalDistance"; + private static final String ATTR_COMPANION_THETA = "orbitalTheta"; private static final String ATTR_DIMID = "DIMID"; private static final String ATTR_NATIVEDIM = "dimMapping"; private static final String ATTR_ICON = "customIcon"; @@ -516,7 +517,9 @@ public static String writeXML(IGalaxy galaxy) { nodeSubStar.setAttribute(ATTR_BLACKHOLE_DISK_ANGLE, Float.toString(star2.diskAngle)); nodeSubStar.setAttribute(ATTR_TEMP, Integer.toString(star2.getTemperature())); nodeSubStar.setAttribute(ATTR_SIZE, Float.toString(star2.getSize())); - nodeSubStar.setAttribute(ATTR_SEPERATION, Float.toString(star2.getStarSeparation())); + nodeSubStar.setAttribute(ATTR_COMPANION_ORBIT, Integer.toString(star2.getOrbitalDistance())); + nodeSubStar.setAttribute(ATTR_COMPANION_THETA, + Double.toString(Math.toDegrees(star2.getBaseTheta()))); nodeStar.appendChild(nodeSubStar); } @@ -1151,7 +1154,7 @@ else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_BIOMEIDS)) { String nbtString = ""; Node weightNode = planetPropertyNode.getAttributes().getNamedItem(ATTR_WEIGHT); Node groupMinNode = planetPropertyNode.getAttributes().getNamedItem(ATTR_GROUPMIN); - Node groupMaxNode = planetPropertyNode.getAttributes().getNamedItem(ATTR_GROUPMIN); + Node groupMaxNode = planetPropertyNode.getAttributes().getNamedItem(ATTR_GROUPMAX); Node nbtNode = planetPropertyNode.getAttributes().getNamedItem(ATTR_NBT); //Get spawn properties @@ -1320,7 +1323,6 @@ else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_CAN_DECORATE) properties.setDecoratoration(Boolean.parseBoolean(planetPropertyNode.getTextContent())); else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_RING_ANGLE)) { properties.ringAngle = Integer.parseInt(planetPropertyNode.getTextContent()); - System.out.println("read rings: "+properties.ringAngle); } else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_RINGCOLOR)) { String[] colors = planetPropertyNode.getTextContent().split(","); @@ -1524,10 +1526,27 @@ public StellarBody readSubStar(Node planetNode) { } } - nameNode = planetNode.getAttributes().getNamedItem(ATTR_SEPERATION); + // A companion's orbit about its primary, in the same distance units a planet's is in. + // It used to be an angle called "separation", which could say how far off the primary a + // companion LOOKED from one particular world and nothing else — not where it was, not + // what it lit, and not that it moved. + nameNode = planetNode.getAttributes().getNamedItem(ATTR_COMPANION_ORBIT); if (nameNode != null && !nameNode.getNodeValue().isEmpty()) { try { - star.setStarSeparation(Float.parseFloat(nameNode.getNodeValue())); + star.setOrbitalDistance(Integer.parseInt(nameNode.getNodeValue())); + } catch (NumberFormatException e) { + AdvancedRocketry.logger.warn("Error Reading star " + star.getName()); + } + } + + nameNode = planetNode.getAttributes().getNamedItem(ATTR_COMPANION_THETA); + if (nameNode != null && !nameNode.getNodeValue().isEmpty()) { + try { + // DEGREES, exactly as a planet's is. One name, one unit: an + // angle that meant radians here and degrees one element away would be a trap + // no author could see, because both parse and neither complains. + star.setBaseTheta(Math.toRadians( + Double.parseDouble(nameNode.getNodeValue()) % 360d)); } catch (NumberFormatException e) { AdvancedRocketry.logger.warn("Error Reading star " + star.getName()); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java index d39acbcd3..b85ea7034 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java @@ -14,6 +14,7 @@ import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.test.MinecraftBootstrap; import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; @@ -122,6 +123,53 @@ public void authoredPlanetsGetTheirOwnCellsInsideTheSuperCellBox() { } } + @Test + public void oneOrbitalDistanceMeansOneDistanceInBothFamilies() { + // The acceptance the scale rework exists for. An authored planet and a procedural one at the + // same orbital distance must stand the same distance from their stars — the field is + // documented in one unit, and every derived number (insolation, temperature, period) is + // computed from it and never from where the body was placed. They used to be turned into + // positions by two different laws: authored linear and absolute, procedural logarithmic and + // normalised to whatever neighbourhood the system had been given. Order survived; proportion + // did not, and the science and the flight time disagreed. + StellarBody star = new StellarBody(); + star.setId(4244); + star.setName("ScaleStar"); + planet(720, 300, 0.0).setStar(star); + + GalacticCoord anchor = GalacticCoord.ofSectorLocal(11, -4, 6, 0, 0, 0); + SystemBody authored = null; + for (SystemBody b : SystemContent.bodiesOf(star, anchor)) { + if (b.dimId() == 720) { + authored = b; + } + } + assertNotNull(authored); + double authoredPerUnit = authored.absoluteAt(0L).distanceTo( + zmaster587.advancedRocketry.space.AbsolutePos.ofCellName(anchor)) + / authored.orbitalDistance(); + + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator( + new GalaxyGenConfig(1.0d, GalaxyGenConfig.DEFAULT_MIN_SPACING, 8, 0.0d, null)); + long spacing = GalaxyGenConfig.DEFAULT_MIN_SPACING; + Optional seat = gen.anchorAt(0xBEEFL, + GalacticCoord.ofSectorLocal(spacing, spacing, spacing, 0L, 0L, 0L)); + assertTrue("the fixture needs an occupied super-cell", seat.isPresent()); + int compared = 0; + for (SystemBody b : gen.bodiesFor(0xBEEFL, seat.get())) { + if (b.kind() != SystemBodyKind.PLANET && b.kind() != SystemBodyKind.GAS_GIANT) { + continue; + } + double proceduralPerUnit = b.absoluteAt(0L).distanceTo( + zmaster587.advancedRocketry.space.AbsolutePos.ofCellName(seat.get())) + / b.orbitalDistance(); + assertEquals("one orbit unit must be one distance in both families", + authoredPerUnit, proceduralPerUnit, authoredPerUnit * 1e-6d); + compared++; + } + assertTrue("the procedural system must have bodies to compare against", compared > 0); + } + @Test public void planetResolvesToItsOwnCellThroughTheRegistry() { StellarBody star = new StellarBody(); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java index 4e3aceec9..ec543fd60 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/AstronomicalBodyHelperTest.java @@ -98,11 +98,54 @@ public void blackHoleStarReducesBrightness() { public void everyStarInASystemContributesItsOwnLight() { double alone = AstronomicalBodyHelper.getStellarBrightness(sunLikeStar(), 100); - StellarBody binary = sunLikeStar(); - binary.addSubStar(sunLikeStar()); + StellarBody contactPair = sunLikeStar(); + StellarBody touching = sunLikeStar(); + touching.setOrbitalDistance(0); // the degenerate case: both stars at the same place + contactPair.addSubStar(touching); - assertEquals("two identical stars light a world twice as brightly as one does", - 2 * alone, AstronomicalBodyHelper.getStellarBrightness(binary, 100), 1e-9); + assertEquals("two identical stars in the same place light a world twice as brightly", + 2 * alone, AstronomicalBodyHelper.getStellarBrightness(contactPair, 100), 1e-9); + } + + @Test + public void aCompanionsContributionFallsOffWithItsOwnDistance() { + // The defect: every companion used to be fed the PRIMARY's distance, so a companion twenty AU + // away warmed a world exactly as much as one sitting beside its star. A separation that costs + // nothing is a separation the model does not really have. + double alone = AstronomicalBodyHelper.getStellarBrightness(sunLikeStar(), 100); + + StellarBody close = sunLikeStar(); + StellarBody nearby = sunLikeStar(); + nearby.setOrbitalDistance(5); // 0.05 AU + close.addSubStar(nearby); + + StellarBody wide = sunLikeStar(); + StellarBody distant = sunLikeStar(); + distant.setOrbitalDistance(2_000); // 20 AU, an Alpha-Centauri-like pair + wide.addSubStar(distant); + + double closeBrightness = AstronomicalBodyHelper.getStellarBrightness(close, 100); + double wideBrightness = AstronomicalBodyHelper.getStellarBrightness(wide, 100); + + assertTrue("a close companion nearly doubles the light", closeBrightness > 1.9 * alone); + assertTrue("a distant one adds only a little", wideBrightness < 1.1 * alone); + assertTrue("but it is never nothing", wideBrightness > alone); + } + + @Test + public void aWorldOfTheCompanionIsLitByThePrimaryToo() { + // An S-type planet is a planet in a binary, not a planet with one sun that happens to have a + // bright neighbour. The walk therefore starts at the system's root, not at the star the + // planet is bound to. + double alone = AstronomicalBodyHelper.getStellarBrightness(sunLikeStar(), 100); + + StellarBody primary = sunLikeStar(); + StellarBody companion = sunLikeStar(); + companion.setOrbitalDistance(0); + primary.addSubStar(companion); + + assertEquals("a world of the companion sees both stars", 2 * alone, + AstronomicalBodyHelper.getStellarBrightness(companion, 100), 1e-9); } /** @@ -123,7 +166,9 @@ public void aCompanionDoesNotTurnABlackHoleBackIntoAStar() { StellarBody holeWithCompanion = sunLikeStar(); holeWithCompanion.setBlackHole(true); - holeWithCompanion.addSubStar(sunLikeStar()); + StellarBody companion = sunLikeStar(); + companion.setOrbitalDistance(0); // separation is not what this test is about + holeWithCompanion.addSubStar(companion); double together = AstronomicalBodyHelper.getStellarBrightness(holeWithCompanion, 100); assertEquals("a black hole and its companion each light the world on their own terms", diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index 4dab7b93e..b47d9d4a2 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -16,6 +16,8 @@ import zmaster587.advancedRocketry.universe.StarSystem; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; +import zmaster587.advancedRocketry.universe.UniverseScale; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -25,9 +27,15 @@ * Contract tests for the deterministic clustered galaxy generator. Pure-JUnit; no MC bootstrap. * *

Pins the generation CONTRACTS: pure determinism over {@code (seed, cell)}, the minimum-spacing - * guarantee, that the distribution actually clusters (void + dense regions), that {@code systemsInRegion} - * agrees cell-for-cell with {@code systemAt}, and that the tunable params drive the outcome. Balance numbers - * are exercised as inputs, never pinned as expected values.

+ * guarantee, the separation floor between two seats, that the distribution actually clusters (void + + * dense regions), that {@code systemsInRegion} agrees with {@code systemAt}, and that the tunable + * params drive the outcome. Balance numbers are exercised as inputs, never pinned as expected + * values.

+ * + *

Sampling is by SUPER-CELL, never by cell. A star seat is one cell in a cube of tens of + * millions, so sweeping cells finds nothing whatever the galaxy holds — and a spacing small enough to + * sweep is a spacing with no room for a system in it, which is a different generator from the shipped + * one. Every sweep here walks the partition the generator itself walks.

*/ public class ClusteredGalaxyGeneratorTest { @@ -37,24 +45,27 @@ private static GalacticCoord cell(long sx, long sy, long sz) { return GalacticCoord.ofSectorLocal(sx, sy, sz, 0L, 0L, 0L); } - /** - * A compact galaxy for sampling tests: the production DEFAULT spacing (a balance number, never pinned) - * is far too sparse to sample in a unit-test-sized volume. - */ - private static GalaxyGenConfig smallCfg() { - return new GalaxyGenConfig(0.35d, 4, 16, 0.6d, null); + /** The shipped spacing: what the sampled galaxy is is what the game ships. */ + private static final int SPACING = GalaxyGenConfig.DEFAULT_MIN_SPACING; + + private static GalaxyGenConfig cfg(double density, int spacing, int clusterScale, double voidFraction) { + return new GalaxyGenConfig(density, spacing, clusterScale, voidFraction, null); + } + + private static GalaxyGenConfig defaultsCfg() { + return cfg(0.35d, SPACING, 16, 0.6d); } - /** Iterate an inclusive sector box, calling the visitor with each cell coordinate. */ + /** Iterate an inclusive box of SUPER-CELLS, calling the visitor with each one's probe cell. */ private interface CellVisitor { void visit(GalacticCoord c); } - private static void forEachCell(long r, CellVisitor v) { + private static void forEachSuperCell(long r, long spacing, CellVisitor v) { for (long x = -r; x <= r; x++) { for (long y = -r; y <= r; y++) { for (long z = -r; z <= r; z++) { - v.visit(cell(x, y, z)); + v.visit(cell(x * spacing, y * spacing, z * spacing)); } } } @@ -62,63 +73,121 @@ private static void forEachCell(long r, CellVisitor v) { @Test public void systemAtIsDeterministic() { - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(smallCfg()); - forEachCell(6, c -> { - Optional a = gen.systemAt(SEED, c); - Optional b = gen.systemAt(SEED, c); - assertEquals("presence must be stable at " + c, a.isPresent(), b.isPresent()); - if (a.isPresent()) { - assertEquals("id stable", a.get().starId(), b.get().starId()); - assertEquals("temperature stable", a.get().star().getTemperature(), - b.get().star().getTemperature()); - assertEquals("size stable", a.get().star().getSize(), b.get().star().getSize(), 0f); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(defaultsCfg()); + forEachSuperCell(6, SPACING, probe -> { + Optional anchor = gen.anchorAt(SEED, probe); + if (!anchor.isPresent()) { + return; } + Optional a = gen.systemAt(SEED, anchor.get()); + Optional b = gen.systemAt(SEED, anchor.get()); + assertTrue("an attributed anchor must point-resolve at " + anchor.get(), a.isPresent()); + assertEquals("presence must be stable", a.isPresent(), b.isPresent()); + assertEquals("id stable", a.get().starId(), b.get().starId()); + assertEquals("temperature stable", a.get().star().getTemperature(), + b.get().star().getTemperature()); + assertEquals("size stable", a.get().star().getSize(), b.get().star().getSize(), 0f); }); } + @Test + public void onlyTheSeatCellItselfHoldsTheSystem() { + // The anchor NAMES the system; its neighbours are ordinary space that merely attributes to it. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(defaultsCfg()); + int checked = 0; + for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 3)) { + assertTrue(gen.systemAt(SEED, anchor).isPresent()); + assertFalse("a cell beside the seat must not itself be the system", + gen.systemAt(SEED, anchor.plusLocal(GalacticCoord.CELL, 0L, 0L)).isPresent()); + checked++; + } + assertTrue(checked > 5); + } + @Test public void differentSeedsProduceDifferentGalaxies() { - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(smallCfg()); - Set occupiedA = occupiedCellKeys(gen, SEED, 8); - Set occupiedB = occupiedCellKeys(gen, SEED + 1, 8); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(defaultsCfg()); + Set occupiedA = occupiedSeats(gen, SEED, SPACING, 6); + Set occupiedB = occupiedSeats(gen, SEED + 1, SPACING, 6); assertFalse("a different seed must not reproduce the same galaxy", occupiedA.equals(occupiedB)); } @Test public void minimumSpacingIsRespected() { // At most one system per minSpacing-cube super-cell, anywhere in the sampled volume. - GalaxyGenConfig cfg = new GalaxyGenConfig(0.9d, 4, 8, 0.0d, null); // dense, no void: stress spacing - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg); + GalaxyGenConfig config = cfg(0.9d, SPACING, 8, 0.0d); // dense, no void: stress spacing + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); Map perSuperCell = new HashMap<>(); - forEachCell(10, c -> { - if (gen.systemAt(SEED, c).isPresent()) { - long s = cfg.minSpacing; - String superKey = Math.floorDiv(c.sectorX(), s) + "_" - + Math.floorDiv(c.sectorY(), s) + "_" + Math.floorDiv(c.sectorZ(), s); - perSuperCell.merge(superKey, 1, Integer::sum); - } - }); + for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 4)) { + long s = config.minSpacing; + String superKey = Math.floorDiv(anchor.sectorX(), s) + "_" + + Math.floorDiv(anchor.sectorY(), s) + "_" + Math.floorDiv(anchor.sectorZ(), s); + perSuperCell.merge(superKey, 1, Integer::sum); + } + assertFalse("the sweep must find systems", perSuperCell.isEmpty()); for (Map.Entry e : perSuperCell.entrySet()) { assertTrue("super-cell " + e.getKey() + " holds " + e.getValue() + " systems (max 1)", e.getValue() <= 1); } } + @Test + public void noTwoStarsStandCloserThanTheSeparationFloor() { + // The floor is what makes a near-pair of seats impossible, and it is what stops two unrelated + // systems — two names, two frames, no gravitational relation — from being read as a binary. + // Multiplicity is something a system states about itself, never something the lattice fakes. + GalaxyGenConfig config = cfg(1.0d, SPACING, 8, 0.0d); // every cube occupied: the tightest case + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); + List seats = anchors(gen, SEED, SPACING, 2); + assertTrue("the sweep must find systems", seats.size() > 10); + double floorBlocks = UniverseScale.SEPARATION_FLOOR_AU * AstronomicalBodyHelper.BLOCKS_PER_AU; + for (int i = 0; i < seats.size(); i++) { + for (int j = i + 1; j < seats.size(); j++) { + double d = seats.get(i).staticFrameDistanceTo(seats.get(j)); + assertTrue("seats " + seats.get(i).cellKey() + " and " + seats.get(j).cellKey() + + " stand " + d + " blocks apart, inside the floor of " + floorBlocks, + d >= floorBlocks); + } + } + } + + @Test + public void aSeatIsNotConfinedToTheMiddleOfItsCube() { + // The seat used to be pinned into the middle quarter per axis — 1.6 % of the cube's volume — + // which reads as a lattice of tight clumps with guaranteed-empty walls. What replaces it is a + // margin sized by what a system NEEDS, so most of the cube is reachable. + GalaxyGenConfig config = cfg(1.0d, SPACING, 8, 0.0d); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); + long s = config.minSpacing; + double nearestFaceFraction = 1d; + int checked = 0; + for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 2)) { + long offset = Math.floorMod(anchor.sectorX(), s); + nearestFaceFraction = Math.min(nearestFaceFraction, offset / (double) s); + nearestFaceFraction = Math.min(nearestFaceFraction, (s - offset) / (double) s); + checked++; + } + assertTrue(checked > 10); + assertTrue("some seat must sit well outside the middle quarter, nearest face fraction was " + + nearestFaceFraction, nearestFaceFraction < 0.25d); + } + @Test public void distributionClustersIntoGalaxiesAndVoid() { - // A strongly-clustered config: expect BOTH occupied sub-regions and entirely-empty (void) sub-regions. - GalaxyGenConfig cfg = new GalaxyGenConfig(0.6d, 2, 8, 0.6d, null); - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg); + // A strongly-clustered config: expect BOTH occupied sub-regions and entirely-empty (void) ones. + GalaxyGenConfig config = cfg(0.6d, SPACING, 8, 0.6d); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); int emptyBlocks = 0; int nonEmptyBlocks = 0; - // Scan 16x16 coarse blocks (each 6x6x1 cells) across a wide plane; classify each as void or populated. + // Scan 16x16 coarse blocks (each 6x6x1 super-cells) across a wide plane. for (long bx = -8; bx < 8; bx++) { for (long by = -8; by < 8; by++) { boolean any = false; for (long dx = 0; dx < 6 && !any; dx++) { for (long dy = 0; dy < 6 && !any; dy++) { - if (gen.systemAt(SEED, cell(bx * 6 + dx, by * 6 + dy, 0)).isPresent()) { + if (gen.anchorAt(SEED, cell((bx * 6 + dx) * SPACING, (by * 6 + dy) * SPACING, 0)) + .isPresent()) { any = true; } } @@ -136,17 +205,21 @@ public void distributionClustersIntoGalaxiesAndVoid() { @Test public void systemsInRegionAgreesWithSystemAt() { - // The single most important consistency contract: the region enumeration and the point query must - // never diverge, or a telescope scan would show systems a jump can't reach (or vice versa). - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(smallCfg()); - long r = 9; - - Set byPointQuery = new HashSet<>(); - forEachCell(r, c -> { - if (gen.systemAt(SEED, c).isPresent()) { - byPointQuery.add(c.cellKey()); + // The single most important consistency contract: the region enumeration and the point query + // must never diverge, or a telescope scan would show systems a jump can't reach (or vice versa). + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(defaultsCfg()); + // The sweep is one super-cell narrower than the box, because a seat sits at an offset INSIDE + // its cube: the outermost swept cube's seat would fall outside a box cut at that cube's face. + long r = 3L * SPACING; + + Set byAttribution = new HashSet<>(); + forEachSuperCell(2, SPACING, probe -> { + Optional anchor = gen.anchorAt(SEED, probe); + if (anchor.isPresent()) { + byAttribution.add(anchor.get().cellKey()); } }); + assertFalse("the sweep must find systems", byAttribution.isEmpty()); Map region = gen.systemsInRegion(SEED, cell(-r, -r, -r), cell(r, r, r)); Set byRegion = new HashSet<>(); @@ -157,34 +230,35 @@ public void systemsInRegionAgreesWithSystemAt() { assertTrue("region cell " + e.getKey() + " must point-resolve", point.isPresent()); assertEquals(point.get().starId(), e.getValue().starId()); } - assertEquals("systemsInRegion must enumerate exactly the point-query occupied cells", - byPointQuery, byRegion); + assertTrue("every seat the sweep attributed must be enumerated by the region query", + byRegion.containsAll(byAttribution)); } @Test public void systemsInRegionHandlesSwappedBounds() { - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(smallCfg()); - Map ordered = gen.systemsInRegion(SEED, cell(-4, -4, -4), cell(4, 4, 4)); - Map swapped = gen.systemsInRegion(SEED, cell(4, 4, 4), cell(-4, -4, -4)); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(defaultsCfg()); + long r = 2L * SPACING; + Map ordered = gen.systemsInRegion(SEED, cell(-r, -r, -r), cell(r, r, r)); + Map swapped = gen.systemsInRegion(SEED, cell(r, r, r), cell(-r, -r, -r)); assertEquals("swapped min/max must enumerate the same box", ordered.keySet(), swapped.keySet()); } @Test public void voidFractionDrivesOccupancy() { - int allVoid = occupiedCellKeys(new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.8d, 2, 8, 1.0d, null)), SEED, 8).size(); - int noVoid = occupiedCellKeys(new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.8d, 2, 8, 0.0d, null)), SEED, 8).size(); + int allVoid = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.8d, SPACING, 8, 1.0d)), + SEED, SPACING, 6).size(); + int noVoid = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.8d, SPACING, 8, 0.0d)), + SEED, SPACING, 6).size(); assertEquals("voidFraction=1 must yield an empty galaxy", 0, allVoid); assertTrue("voidFraction=0 must populate the galaxy", noVoid > 0); } @Test public void densityDrivesOccupancy() { - int sparse = occupiedCellKeys(new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.1d, 2, 8, 0.0d, null)), SEED, 10).size(); - int dense = occupiedCellKeys(new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.9d, 2, 8, 0.0d, null)), SEED, 10).size(); + int sparse = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.1d, SPACING, 8, 0.0d)), + SEED, SPACING, 7).size(); + int dense = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.9d, SPACING, 8, 0.0d)), + SEED, SPACING, 7).size(); assertTrue("higher density must place more systems (" + sparse + " vs " + dense + ")", dense > sparse); } @@ -195,22 +269,22 @@ public void starTypesAreDrawnFromTheConfiguredSetAndWeighted() { List types = new ArrayList<>(); types.add(new GalaxyGenConfig.StarType(50, 0.5f, 1.0f, 100)); // common types.add(new GalaxyGenConfig.StarType(250, 2.0f, 3.0f, 1)); // rare - GalaxyGenConfig cfg = new GalaxyGenConfig(0.9d, 1, 8, 0.0d, types); - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg); + GalaxyGenConfig config = new GalaxyGenConfig(0.9d, SPACING, 8, 0.0d, types); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); int common = 0; int rare = 0; int other = 0; int total = 0; Set seenTemps = new HashSet<>(); - // Iterate the underlying loop directly for a large sample. for (long x = -20; x <= 20; x++) { for (long y = -20; y <= 20; y++) { - Optional sys = gen.systemAt(SEED, cell(x, y, 0)); - if (!sys.isPresent()) { + Optional anchor = gen.anchorAt(SEED, cell(x * SPACING, y * SPACING, 0)); + if (!anchor.isPresent()) { continue; } - int temp = sys.get().star().getTemperature(); + StarSystem sys = gen.systemAt(SEED, anchor.get()).get(); + int temp = sys.star().getTemperature(); seenTemps.add(Integer.toString(temp)); total++; if (temp == 50) { @@ -221,7 +295,7 @@ public void starTypesAreDrawnFromTheConfiguredSetAndWeighted() { other++; } // size must lie in the archetype's range - float size = sys.get().star().getSize(); + float size = sys.star().getSize(); if (temp == 50) { assertTrue(size >= 0.5f && size <= 1.0f); } else if (temp == 250) { @@ -237,16 +311,12 @@ public void starTypesAreDrawnFromTheConfiguredSetAndWeighted() { @Test public void proceduralSystemIdsAreNegative() { - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.9d, 1, 8, 0.0d, null)); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(0.9d, SPACING, 8, 0.0d)); boolean sawAny = false; - for (long x = -10; x <= 10; x++) { - Optional sys = gen.systemAt(SEED, cell(x, 0, 0)); - if (sys.isPresent()) { - sawAny = true; - assertTrue("procedural systems must carry a synthetic negative id, got " + sys.get().starId(), - sys.get().starId() < 0); - } + for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 2)) { + sawAny = true; + assertTrue("procedural systems must carry a synthetic negative id", + gen.systemAt(SEED, anchor).get().starId() < 0); } assertTrue(sawAny); } @@ -276,14 +346,15 @@ public void hugeStarWeightsDoNotCollapseTheDistribution() { types.add(new GalaxyGenConfig.StarType(50, 0.5f, 1.0f, Integer.MAX_VALUE)); types.add(new GalaxyGenConfig.StarType(250, 2.0f, 3.0f, Integer.MAX_VALUE)); ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.9d, 1, 8, 0.0d, types)); + new GalaxyGenConfig(0.9d, SPACING, 8, 0.0d, types)); Set seenTemps = new HashSet<>(); for (long x = -20; x <= 20; x++) { for (long y = -20; y <= 20; y++) { - Optional sys = gen.systemAt(SEED, cell(x, y, 0)); - if (sys.isPresent()) { - seenTemps.add(Integer.toString(sys.get().star().getTemperature())); + Optional anchor = gen.anchorAt(SEED, cell(x * SPACING, y * SPACING, 0)); + if (anchor.isPresent()) { + seenTemps.add(Integer.toString( + gen.systemAt(SEED, anchor.get()).get().star().getTemperature())); } } } @@ -293,24 +364,18 @@ public void hugeStarWeightsDoNotCollapseTheDistribution() { @Test public void proceduralBodiesGetTheirOwnCellsInsideTheSuperCell() { - // A#1a: a system is an anchored NEIGHBOURHOOD — the star holds the anchor cell, each planet/belt - // its own cell (snapped to that cell's centre), all inside the anchor's minSpacing super-cell. - GalaxyGenConfig cfg = new GalaxyGenConfig(0.9d, 16, 8, 0.0d, null); - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg); - long s = cfg.minSpacing; + // A system is an anchored NEIGHBOURHOOD — the star holds the anchor cell, each planet/belt its + // own cell (snapped to that cell's centre), all inside the anchor's minSpacing super-cell. + GalaxyGenConfig config = cfg(0.9d, SPACING, 8, 0.0d); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); + long s = config.minSpacing; boolean checkedAny = false; - for (long sup = -3; sup <= 3; sup++) { - GalacticCoord probe = cell(sup * s, 0, 0); - java.util.Optional anchorOpt = gen.anchorAt(SEED, probe); - if (!anchorOpt.isPresent()) { - continue; - } + for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 1)) { checkedAny = true; - GalacticCoord anchor = anchorOpt.get(); List a = gen.bodiesFor(SEED, anchor); assertEquals("bodiesFor must be deterministic", a, gen.bodiesFor(SEED, anchor)); assertEquals("bodiesFor must accept a member cell and answer for the whole system", - a, gen.bodiesFor(SEED, probe)); + a, gen.bodiesFor(SEED, anchor.plusLocal(GalacticCoord.CELL, 0L, 0L))); assertFalse("an occupied system must have bodies", a.isEmpty()); assertEquals("first body is the star at the anchor", SystemBodyKind.STAR, a.get(0).kind()); @@ -338,18 +403,63 @@ public void proceduralBodiesGetTheirOwnCellsInsideTheSuperCell() { assertTrue(checkedAny); } + @Test + public void aBodyStandsExactlyWhereItsOrbitalDistanceSaysItDoes() { + // The acceptance the whole scale rework exists for: ONE law, ONE constant. A body at orbital + // distance d is d units from its star, in blocks, and its cell NAME is a reading of that same + // position rather than a second layout arithmetic beside it. When those two came apart, the + // science said one thing and the flight time said another. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(0.9d, SPACING, 8, 0.0d)); + int checked = 0; + for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 1)) { + List bodies = gen.bodiesFor(SEED, anchor); + SystemBody star = bodies.get(0); + for (SystemBody body : bodies) { + if (body.kind() == SystemBodyKind.STAR || body.kind() == SystemBodyKind.MOON + || body.kind() == SystemBodyKind.ASTEROID_BELT) { + continue; + } + double expected = (double) body.orbitalDistance() + * AstronomicalBodyHelper.BLOCKS_PER_ORBIT_UNIT; + double placed = body.absoluteAt(0L).distanceTo(star.absoluteAt(0L)); + assertEquals("body at orbit " + body.orbitalDistance() + " of system " + + anchor.cellKey() + " must stand that far from its star", + expected, placed, expected * 1e-6d + 2d); + // And the cell it is NAMED by is a reading of that same place, to within a cell. + double named = body.name().staticFrameDistanceTo(anchor); + assertTrue("the body's cell name (" + named + " blocks out) must agree with where it " + + "is (" + placed + ")", + Math.abs(named - placed) <= 2d * GalacticCoord.CELL); + checked++; + } + } + assertTrue("the sweep must find bodies", checked > 10); + } + + @Test + public void aSystemNeverReachesPastItsOwnClearSpace() { + // The bound that replaces "a system is a fraction of the distance to the next star": named + // bodies stay inside half the separation floor, whatever a star's own zone would have drawn. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(1.0d, SPACING, 8, 0.0d)); + int checked = 0; + for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 1)) { + for (SystemBody body : gen.bodiesFor(SEED, anchor)) { + assertTrue("body at orbit " + body.orbitalDistance() + " reaches past its system's " + + "clear space of " + UniverseScale.MAX_NAMED_ORBIT_UNITS + " units", + body.orbitalDistance() <= UniverseScale.MAX_NAMED_ORBIT_UNITS); + checked++; + } + } + assertTrue(checked > 10); + } + @Test public void tinySpacingDegeneratesIntoALoneStar() { // minSpacing=1: the super-cell IS one cell, and the star already holds it. A second real body // would have to share that cell, which at most one real body per cell forbids — so the system // degenerates to its star alone. Degenerate but CONSISTENT: attribution stays exact, nothing // escapes the box, and no cell ends up with two destinations in it. - // - // (Before the retinue gained a distinctness rule this read "every body clamps into the anchor - // cell", which was the same arrangement described from the other side — and describing it that - // way made the invariant violation sound like the intended behaviour.) - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.9d, 1, 8, 0.0d, null)); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(0.9d, 1, 8, 0.0d)); boolean checkedAny = false; for (long x = -6; x <= 6; x++) { GalacticCoord c = cell(x, 0, 0); @@ -368,12 +478,12 @@ public void tinySpacingDegeneratesIntoALoneStar() { @Test public void anchorAtAttributesEveryCellOfAnOccupiedSuperCell() { - GalaxyGenConfig cfg = new GalaxyGenConfig(0.9d, 8, 8, 0.0d, null); - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg); - long s = cfg.minSpacing; + GalaxyGenConfig config = cfg(0.9d, SPACING, 8, 0.0d); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); + long s = config.minSpacing; boolean checkedAny = false; for (long sup = -2; sup <= 2; sup++) { - java.util.Optional anchor = gen.anchorAt(SEED, cell(sup * s, 0, 0)); + Optional anchor = gen.anchorAt(SEED, cell(sup * s, 0, 0)); if (!anchor.isPresent()) { continue; } @@ -383,7 +493,7 @@ public void anchorAtAttributesEveryCellOfAnOccupiedSuperCell() { for (long dy : new long[] {0, s - 1}) { GalacticCoord member = cell(sup * s + dx, dy, 0); assertEquals("member " + member + " must attribute to the super-cell's anchor", - java.util.Optional.of(anchor.get()), gen.anchorAt(SEED, member)); + anchor, gen.anchorAt(SEED, member)); } } // The anchor itself point-resolves to the system. @@ -394,13 +504,26 @@ public void anchorAtAttributesEveryCellOfAnOccupiedSuperCell() { // ─── helpers ─────────────────────────────────────────────────────────────── - private static Set occupiedCellKeys(ClusteredGalaxyGenerator gen, long seed, long r) { - Set keys = new HashSet<>(); - forEachCell(r, c -> { - if (gen.systemAt(seed, c).isPresent()) { - keys.add(c.cellKey()); + /** Every distinct seat in a sweep of super-cells. */ + private static List anchors(ClusteredGalaxyGenerator gen, long seed, long spacing, + long r) { + Set seen = new HashSet<>(); + List out = new ArrayList<>(); + forEachSuperCell(r, spacing, probe -> { + Optional a = gen.anchorAt(seed, probe); + if (a.isPresent() && seen.add(a.get().cellKey())) { + out.add(a.get()); } }); + return out; + } + + private static Set occupiedSeats(ClusteredGalaxyGenerator gen, long seed, long spacing, + long r) { + Set keys = new HashSet<>(); + for (GalacticCoord a : anchors(gen, seed, spacing, r)) { + keys.add(a.cellKey()); + } return keys; } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java index 7aa5c4a2f..c62429627 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java @@ -434,18 +434,24 @@ public void orbitsAreOrderedAndSpreadLogarithmically() { } @Test - public void theCellPlacementFractionAgreesWithTheOrbitItCameFrom() { - // The placement maps an orbit onto a cell radius through this fraction, so a body that is third - // from its star is third out from the anchor cell. If the two ever disagreed, the sky would show - // a system laid out differently from the one the physics describes. - StellarBody s = sol(); - double previous = -1d; - for (int d = 1; d <= 20_000; d += 37) { - double f = PlanetDerivation.orbitFraction(d, s); - assertTrue("fraction must stay in [0,1]", f >= 0d && f <= 1d); - assertTrue("fraction must not decrease as the orbit grows", f >= previous); - previous = f; + public void aStarsZoneIsItsOwnBusinessAndNotItsNeighbourhoods() { + // How much room a system has where it happens to sit is not an input to where its worlds + // orbit. A cramped system holds FEWER worlds — the generator drops what does not fit — and + // never the same worlds moved closer to their star than their own climate says they are. + // The defect this replaces normalised every orbit to the neighbourhood, so one orbital + // distance was one distance in a roomy system and another in a cramped one. + StellarBody dwarf = star(40, 0.6f); + StellarBody giant = star(220, 2.6f); + GalacticCoord anchor = cell(4, -2, 7); + + for (int i = 0; i < 6; i++) { + int cool = PlanetDerivation.orbitalDistanceOf(SEED, anchor, i, 6, dwarf); + int hot = PlanetDerivation.orbitalDistanceOf(SEED, anchor, i, 6, giant); + assertTrue("a hot star's zone must be wider than a cool one's at every rank (" + + cool + " vs " + hot + ")", hot > cool); } + assertTrue("a cool dwarf's system is compact", + PlanetDerivation.outerOrbit(dwarf) < PlanetDerivation.outerOrbit(giant)); } // ─── D6: the availability filter runs BEFORE the draw ────────────────────── diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java index 5531c6dff..a89f3c3e8 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java @@ -41,37 +41,77 @@ public void resetSeams() { UniverseRegistry.setStarLookup(null); } - /** - * A dense, void-free galaxy so a small sweep is guaranteed to find systems. The spacing is - * deliberately tiny: a system's anchor is seated in the MIDDLE BAND of its super-cell, so at the - * production spacing of 512 the nearest anchor is hundreds of cells from the origin and a - * unit-test-sized sweep finds an empty universe. - */ + /** The shipped spacing: a system sampled here is a system the game ships. */ + private static final int SPACING = GalaxyGenConfig.DEFAULT_MIN_SPACING; + + /** A dense, void-free galaxy, so the first super-cell probed holds a system. */ private static UniverseRegistry registryWithProceduralGalaxy() { UniverseRegistry reg = new UniverseRegistry(); UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.9d, 4, 8, 0.0d, null))); + new GalaxyGenConfig(1.0d, SPACING, 8, 0.0d, null))); reg.bindWorldSeed(SEED); return reg; } - /** The first cell in a small sweep holding a body a ship could land on but that has no world. */ + /** + * The seat of a system near the origin. + * + *

Probed one SUPER-CELL at a time, never cell by cell: a star's seat is one cell in a cube of + * tens of millions, so a sweep of adjacent cells finds nothing however full the galaxy is. The + * partition is the thing to walk, and it is what the generator itself walks.

+ */ + private static GalacticCoord systemAnchor(UniverseRegistry reg) { + for (long i = 0; i <= 8; i++) { + Optional anchor = reg.anchorForCell( + GalacticCoord.ofSectorLocal(i * SPACING, 0L, 0L, 0L, 0L, 0L)); + if (anchor.isPresent()) { + return anchor.get(); + } + } + return null; + } + + /** The cell of the first body in that system a ship could land on but that has no world yet. */ private static GalacticCoord findLandableCell(UniverseRegistry reg) { - for (long x = -8; x <= 8; x++) { - for (long y = -8; y <= 8; y++) { - for (long z = -8; z <= 8; z++) { - GalacticCoord cell = GalacticCoord.ofSectorLocal(x, y, z, 0L, 0L, 0L); - for (SystemBody b : reg.bodiesAt(cell)) { - if (b.kind().canDescend() && b.dimId() == Constants.INVALID_PLANET) { - return cell; - } - } - } + GalacticCoord anchor = systemAnchor(reg); + if (anchor == null) { + return null; + } + for (SystemBody b : reg.systemBodiesAt(anchor)) { + if (b.kind().canDescend() && b.dimId() == Constants.INVALID_PLANET) { + return b.name(); } } return null; } + /** + * The first {@code (parent, moon)} pair found in a sweep of nearby systems, or {@code null}s. + * + *

Several systems, because moons are a draw: most bodies have none and a giant has several, so + * one system is not guaranteed to hold a pair and a fixture that assumed it would be flaky for a + * reason that has nothing to do with what it tests.

+ */ + private static SystemBody[] findPlanetWithMoon(UniverseRegistry reg) { + for (long i = 0; i <= 8; i++) { + Optional seat = reg.anchorForCell( + GalacticCoord.ofSectorLocal(i * SPACING, 0L, 0L, 0L, 0L, 0L)); + if (!seat.isPresent()) { + continue; + } + SystemBody parent = null; + for (SystemBody b : reg.systemBodiesAt(seat.get())) { + if (b.kind() != SystemBodyKind.MOON && b.kind().canDescend()) { + parent = b; + } else if (b.kind() == SystemBodyKind.MOON && parent != null + && b.name().sameCell(parent.name())) { + return new SystemBody[] {parent, b}; + } + } + } + return new SystemBody[] {null, null}; + } + @Test public void theProceduralGalaxyOffersLandableBodiesThatHaveNoWorldYet() { // The precondition of everything below, and the defect the whole batch exists to fix: the @@ -99,25 +139,9 @@ public void theProceduralGalaxyOffersLandableBodiesThatHaveNoWorldYet() { @Test public void aMoonCarriesItsOwnDistanceFromItsParentSeparatelyFromItsParentsFromTheStar() { UniverseRegistry reg = registryWithProceduralGalaxy(); - SystemBody moon = null; - SystemBody itsParent = null; - outer: - for (long x = -8; x <= 8 && moon == null; x++) { - for (long y = -8; y <= 8; y++) { - for (long z = -8; z <= 8; z++) { - SystemBody parent = null; - for (SystemBody b : reg.bodiesAt(GalacticCoord.ofSectorLocal(x, y, z, 0L, 0L, 0L))) { - if (parent == null && b.kind() != SystemBodyKind.MOON && b.kind().canDescend()) { - parent = b; - } else if (b.kind() == SystemBodyKind.MOON && parent != null) { - moon = b; - itsParent = parent; - break outer; - } - } - } - } - } + SystemBody[] pair = findPlanetWithMoon(reg); + SystemBody itsParent = pair[0]; + SystemBody moon = pair[1]; assertNotNull("the procedural galaxy must produce a moon to test with", moon); assertNotNull(itsParent); @@ -146,25 +170,9 @@ public void aMoonCarriesItsOwnDistanceFromItsParentSeparatelyFromItsParentsFromT @Test public void aProceduralPlanetOrbitsItsStarAndItsMoonsTravelWithIt() { UniverseRegistry reg = registryWithProceduralGalaxy(); - SystemBody planet = null; - SystemBody moon = null; - outer: - for (long x = -8; x <= 8; x++) { - for (long y = -8; y <= 8; y++) { - for (long z = -8; z <= 8; z++) { - SystemBody candidate = null; - for (SystemBody b : reg.bodiesAt(GalacticCoord.ofSectorLocal(x, y, z, 0L, 0L, 0L))) { - if (candidate == null && b.kind() != SystemBodyKind.MOON && b.kind().canDescend()) { - candidate = b; - } else if (b.kind() == SystemBodyKind.MOON && candidate != null) { - planet = candidate; - moon = b; - break outer; - } - } - } - } - } + SystemBody[] pair = findPlanetWithMoon(reg); + SystemBody planet = pair[0]; + SystemBody moon = pair[1]; assertNotNull("the procedural galaxy must produce a planet with a moon", planet); assertNotNull(moon); @@ -266,7 +274,7 @@ public void aPinnedSystemsStarSurvivesAChangeOfGenerator() { // A pack edit: a different spacing, a different density, a whole different galaxy. UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.2d, 32, 4, 0.5d, null))); + new GalaxyGenConfig(0.2d, SPACING / 2, 4, 0.5d, null))); Optional after = reg.starAt(cell); assertTrue(after.isPresent()); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/StellarHierarchyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/StellarHierarchyTest.java new file mode 100644 index 000000000..9a1de99c9 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/StellarHierarchyTest.java @@ -0,0 +1,188 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import net.minecraft.nbt.NBTTagCompound; + +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * What the star model can EXPRESS: a single star, a close pair, a wide pair, and a hierarchy three + * deep — each one placed, lit and round-tripped without a special case for its shape. + * + *

The model could nest companions in storage long before it could mean anything by them. A + * companion was given its primary's id, so no {@code starId} could address it and it could own no + * world; its separation was an angle, so nothing could say where it was; and every companion was lit + * as though it stood exactly where its primary does. These pin the shape that replaced that, never + * the balance numbers: what is asserted is that a distance is a distance, that identity is per star, + * and that light falls off with the separation it is given.

+ */ +public class StellarHierarchyTest { + + private static StellarBody star(String name, float size) { + StellarBody s = new StellarBody(); + s.setName(name); + s.setSize(size); + s.setTemperature(100); + return s; + } + + // ─── identity ────────────────────────────────────────────────────────────── + + @Test + public void bindingACompanionLeavesItsIdentityAlone() { + // The whole reason a companion could own nothing: it was handed its primary's id, and a + // planet binds to its star by that number. Minting one is the registry's job; binding is not + // allowed to overwrite what the registry handed out. + StellarBody primary = star("A", 1f); + primary.setId(7); + StellarBody companion = star("B", 0.5f); + companion.setId(19); + + primary.addSubStar(companion); + + assertEquals("the primary keeps its id", 7, primary.getId()); + assertEquals("and so does the companion", 19, companion.getId()); + assertSame("which now knows what it orbits", primary, companion.getParentStar()); + } + + @Test + public void aCompanionAnswersForItsOwnWorldsAndNotItsPrimarys() { + StellarBody primary = star("A", 1f); + StellarBody companion = star("B", 0.5f); + primary.addSubStar(companion); + + assertEquals("a companion with no worlds holds none", 0, companion.getNumPlanets()); + assertEquals("and the primary's count is its own", 0, primary.getNumPlanets()); + } + + // ─── placement ───────────────────────────────────────────────────────────── + + @Test + public void aCompanionStandsWhereItsOrbitSaysAndAPrimaryAtTheOrigin() { + StellarBody primary = star("A", 1f); + StellarBody companion = star("B", 0.5f); + companion.setOrbitalDistance(2_000); // 20 AU + companion.setBaseTheta(0d); + primary.addSubStar(companion); + + assertEquals("a primary defines its system's origin", 0d, + primary.offsetFromSystemAu()[0], 0d); + assertEquals(20d, companion.offsetFromSystemAu()[0], 1e-9); + assertEquals(20d, companion.separationAuFrom(primary), 1e-9); + assertEquals("separation is symmetric", 20d, primary.separationAuFrom(companion), 1e-9); + } + + @Test + public void aThreeStarHierarchyComposesRatherThanSpecialCases() { + // B orbits A at 20 AU; C orbits B at 5 AU on the same bearing. C is 25 AU from A, and the + // arithmetic that says so is the same one a pair uses. + StellarBody a = star("A", 1f); + StellarBody b = star("B", 0.8f); + StellarBody c = star("C", 0.3f); + b.setOrbitalDistance(2_000); + b.setBaseTheta(0d); + c.setOrbitalDistance(500); + c.setBaseTheta(0d); + a.addSubStar(b); + b.addSubStar(c); + + assertEquals(25d, c.separationAuFrom(a), 1e-9); + assertEquals(5d, c.separationAuFrom(b), 1e-9); + assertEquals("every star of the system is reached from any of them", + 3, AstronomicalBodyHelper.systemOf(c).size()); + } + + @Test + public void unstatedCompanionPhasesAreSpreadRatherThanStacked() { + // Two companions on the same bearing would be one object as far as every consumer is + // concerned. Nothing here says WHICH angles they get — only that binding gives them + // different ones when nobody has said. + StellarBody primary = star("A", 1f); + StellarBody first = star("B", 0.5f); + StellarBody second = star("C", 0.5f); + primary.addSubStar(first); + primary.addSubStar(second); + + assertNotEquals(first.getBaseTheta(), second.getBaseTheta(), 1e-9); + } + + @Test + public void anAuthoredPhaseSurvivesBinding() { + StellarBody primary = star("A", 1f); + StellarBody companion = star("B", 0.5f); + companion.setBaseTheta(1.25d); + primary.addSubStar(companion); + + assertEquals(1.25d, companion.getBaseTheta(), 0d); + } + + // ─── the sky ─────────────────────────────────────────────────────────────── + + @Test + public void apparentSeparationIsARealAngleFromARealDistance() { + StellarBody primary = star("A", 1f); + StellarBody close = star("B", 0.5f); + close.setOrbitalDistance(5); // 0.05 AU + primary.addSubStar(close); + + StellarBody other = star("C", 1f); + StellarBody wide = star("D", 0.5f); + wide.setOrbitalDistance(2_000); // 20 AU + other.addSubStar(wide); + + float closeAngle = close.apparentSeparationDegrees(100); + float wideAngle = wide.apparentSeparationDegrees(100); + + assertTrue("a close pair reads as two suns almost together, saw " + closeAngle, + closeAngle > 0f && closeAngle < 10f); + assertTrue("a wide companion is somewhere else in the sky entirely, saw " + wideAngle, + wideAngle > 60f); + assertEquals("a star nobody orbits has no separation from itself", 0f, + primary.apparentSeparationDegrees(100), 0f); + } + + // ─── round trip ──────────────────────────────────────────────────────────── + + @Test + public void aHierarchyRoundTripsThroughNBTWithItsGeometry() { + StellarBody a = star("A", 1f); + a.setId(3); + StellarBody b = star("B", 0.8f); + b.setId(4); + b.setOrbitalDistance(2_000); + b.setBaseTheta(0.75d); + StellarBody c = star("C", 0.3f); + c.setId(5); + c.setOrbitalDistance(500); + c.setBaseTheta(2.5d); + a.addSubStar(b); + b.addSubStar(c); + + NBTTagCompound nbt = new NBTTagCompound(); + a.writeToNBT(nbt); + StellarBody read = new StellarBody(); + read.readFromNBT(nbt); + + assertEquals(1, read.getSubStars().size()); + StellarBody readB = read.getSubStars().get(0); + assertEquals("a companion's own id survives", 4, readB.getId()); + assertEquals(2_000, readB.getOrbitalDistance()); + assertEquals(0.75d, readB.getBaseTheta(), 1e-9); + assertSame("and it still knows what it orbits", read, readB.getParentStar()); + + assertEquals(1, readB.getSubStars().size()); + StellarBody readC = readB.getSubStars().get(0); + assertEquals(5, readC.getId()); + assertEquals(500, readC.getOrbitalDistance()); + assertEquals(2.5d, readC.getBaseTheta(), 1e-9); + assertEquals("the geometry survives to the third star", c.separationAuFrom(a), + readC.separationAuFrom(read), 1e-9); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java index 7064dfb2b..cda845d35 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java @@ -12,9 +12,11 @@ import java.util.Optional; import java.util.Set; +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.PlanetDerivation; import zmaster587.advancedRocketry.universe.PlanetTypes; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; @@ -47,11 +49,17 @@ private static GalacticCoord cell(long sx, long sy, long sz) { return GalacticCoord.ofSectorLocal(sx, sy, sz, 0L, 0L, 0L); } + /** The shipped spacing: a system laid out here is the system the game ships. */ + private static final int SPACING = GalaxyGenConfig.DEFAULT_MIN_SPACING; + /** - * A galaxy dense enough to sample and roomy enough to lay a system out in. The spacing has to leave - * a real neighbourhood: a body's cell radius is bounded by {@code 3s/8}, so at {@code s=4} a system - * has a single ring of cells to put a dozen bodies in. + * A spacing tight enough that a system's own clear space, not its star's zone, decides how far its + * outermost body may sit. It is where the collision risk bites, because every body is squeezed into + * far fewer distinct cells. */ + private static final int CRAMPED_SPACING = 1_000; + + /** A galaxy dense enough to sample: every cube occupied, so a small sweep finds many systems. */ private static ClusteredGalaxyGenerator gen(int minSpacing) { return new ClusteredGalaxyGenerator(new GalaxyGenConfig(0.9d, minSpacing, 8, 0.0d, null)); } @@ -82,7 +90,7 @@ public void noTwoRealBodiesOfOneSystemShareACell() { // Measured the way SystemContent.auditOneRealBodyPerCell measures it — moons exempt, because a // moon lives in its parent's cell by construction — so the generator and the audit cannot // disagree silently about what the invariant says. - int minSpacing = 64; + int minSpacing = SPACING; ClusteredGalaxyGenerator g = gen(minSpacing); int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { @@ -107,7 +115,7 @@ public void theInvariantHoldsEvenWhenTheNeighbourhoodIsCrampedForRoom() { // The collision risk grows with the square of the body count, so the tightest spacing that still // has more than one cell is where it bites. A cramped system is allowed to hold FEWER bodies; // it is not allowed to hold two in one cell. - int minSpacing = 8; + int minSpacing = CRAMPED_SPACING; ClusteredGalaxyGenerator g = gen(minSpacing); int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { @@ -124,6 +132,66 @@ public void theInvariantHoldsEvenWhenTheNeighbourhoodIsCrampedForRoom() { assertTrue(checked > 5); } + // ─── what a system loses when it does not fit ────────────────────────────── + + @Test + public void atTheShippedScaleNoSystemLosesABodyAtAll() { + // The bound is a GUARD, not a mechanic anybody meets. Measured 2026-08-14: the widest zone + // any shipped star archetype can draw is 569 AU against a clear space of 5 000 — a factor of + // nearly nine. If this ever goes red, either the star table gained something far hotter or + // the spacing was cut by two orders, and both are worth knowing about deliberately. + ClusteredGalaxyGenerator g = gen(SPACING); + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { + int wanted = ClusteredGalaxyGenerator.retinueSize(SEED, anchor); + int got = 0; + for (SystemBody b : g.bodiesFor(SEED, anchor)) { + if (b.kind() == SystemBodyKind.PLANET || b.kind() == SystemBodyKind.GAS_GIANT) { + got++; + } + } + assertEquals("system " + anchor.cellKey() + " lost a body it had room for", wanted, got); + checked++; + } + assertTrue(checked > 10); + } + + @Test + public void aCrampedSystemDropsBodiesAndNeverMovesTheOnesItKeeps() { + // The distinction the whole placement seam exists for. A system squeezed by its neighbours + // holds FEWER worlds; it does not hold the same worlds at distances their own climate, + // insolation and year do not describe. So every body a cramped system keeps must stand at an + // orbit the star's own zone drew, unchanged — never at one scaled to fit the room. + ClusteredGalaxyGenerator g = gen(CRAMPED_SPACING); + int droppedSomewhere = 0; + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, CRAMPED_SPACING, 2)) { + StellarBody star = g.systemAt(SEED, anchor).get().star(); + int count = ClusteredGalaxyGenerator.retinueSize(SEED, anchor); + Set drawn = new HashSet<>(); + for (int i = 0; i < count; i++) { + drawn.add(PlanetDerivation.orbitalDistanceOf(SEED, anchor, i, count, star)); + } + int kept = 0; + for (SystemBody b : g.bodiesFor(SEED, anchor)) { + if (b.kind() != SystemBodyKind.PLANET && b.kind() != SystemBodyKind.GAS_GIANT) { + continue; + } + kept++; + assertTrue("a kept body stands at orbit " + b.orbitalDistance() + + ", which its star never drew — it was moved to fit", + drawn.contains(b.orbitalDistance())); + } + if (kept < count) { + droppedSomewhere++; + } + checked++; + } + assertTrue(checked > 10); + assertTrue("the cramped fixture must actually be cramped, or this proves nothing", + droppedSomewhere > 0); + } + // ─── E1: a long-tailed body count ────────────────────────────────────────── @Test @@ -169,7 +237,7 @@ public void theRetinueSizeIsDeterministic() { public void everySystemEndsInABelt() { // Load-bearing beyond this task: drifting out of jump range is only survivable because every // system has something to mine without landing. "Usually" would be a soft-lock. - int minSpacing = 64; + int minSpacing = SPACING; ClusteredGalaxyGenerator g = gen(minSpacing); int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { @@ -198,7 +266,7 @@ public void anInnerBeltAppearsOnlyWhereAGiantClearedOne() { // A belt is material a giant's resonances stopped from accreting, so a second belt inside the // system implies a giant. The converse is not asserted: a giant near the edge has no room for a // gap inside it, and a cramped neighbourhood may have no free cell to put one in. - int minSpacing = 64; + int minSpacing = SPACING; ClusteredGalaxyGenerator g = gen(minSpacing); int systemsWithInnerBelt = 0; for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { @@ -227,7 +295,7 @@ public void anInnerBeltAppearsOnlyWhereAGiantClearedOne() { public void moonsExistAndLiveInsideTheirParentsCell() { // Without moons the whole outer system is look-only: nothing out there is landable, because the // bodies big enough to be out there are the ones with no surface. - int minSpacing = 64; + int minSpacing = SPACING; ClusteredGalaxyGenerator g = gen(minSpacing); int moons = 0; int checked = 0; @@ -258,7 +326,7 @@ public void moonsExistAndLiveInsideTheirParentsCell() { public void aMoonIsSomewhereElseInsideItsCellThanItsParent() { // A moon that never moved inside the cell would be at the cell centre, i.e. exactly where the // planet is — one address, two bodies, and a descent that cannot say which it came for. - int minSpacing = 64; + int minSpacing = SPACING; ClusteredGalaxyGenerator g = gen(minSpacing); boolean checkedAny = false; for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 3)) { @@ -283,7 +351,7 @@ public void aSystemsCellLayoutFollowsItsOrbits() { // The cell radius is derived from the orbit, so a body further from its star is further from the // anchor cell. If the two ever came apart, the map would show a system laid out differently from // the one the physics describes. - int minSpacing = 128; + int minSpacing = SPACING; ClusteredGalaxyGenerator g = gen(minSpacing); int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 2)) { @@ -317,7 +385,7 @@ public void aSystemsCellLayoutFollowsItsOrbits() { @Test public void theWholeRetinueIsDeterministic() { - int minSpacing = 64; + int minSpacing = SPACING; ClusteredGalaxyGenerator g = gen(minSpacing); int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, minSpacing, 2)) { diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java index 5ac4ddd5d..611927cb3 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java @@ -41,6 +41,13 @@ */ public class UniverseRegistryTest { + /** + * A sector far enough away to be a DIFFERENT system's territory. An anchor owns every cell of its + * super-cell, so "elsewhere" has to be stated in super-cells; a literal few thousand sectors is + * the same neighbourhood, and a fixture using one proves nothing about attribution. + */ + private static final long ANOTHER_SUPER_CELL = 2L * GalaxyGenConfig.DEFAULT_MIN_SPACING; + private static StellarBody star(int id) { StellarBody s = new StellarBody(); s.setId(id); @@ -263,7 +270,7 @@ public void systemForCoordPrefersStoredOverGenerator() { // A cell in a DIFFERENT super-cell falls through to the generator. Optional farAway = reg.systemForCoord( - GalacticCoord.ofSectorLocal(4_000, 4_000, 4_000, 0, 0, 0)); + GalacticCoord.ofSectorLocal(ANOTHER_SUPER_CELL, ANOTHER_SUPER_CELL, ANOTHER_SUPER_CELL, 0, 0, 0)); assertTrue(farAway.isPresent()); assertEquals(777, farAway.get().starId()); } @@ -557,7 +564,7 @@ public void aRecycledDimensionIdDoesNotInheritTheOldBodysName() { UniverseRegistry reg = new UniverseRegistry(); reg.place(GalacticCoord.ORIGIN, 6001); - reg.place(GalacticCoord.ofSectorLocal(4000, 0, 0, 0, 0, 0), 6002); + reg.place(GalacticCoord.ofSectorLocal(ANOTHER_SUPER_CELL, 0, 0, 0, 0, 0), 6002); DimensionProperties original = bodyOfStar(sol, 6100, 150, 0.3); Optional firstName = reg.coordForPlanet(original); @@ -575,7 +582,7 @@ public void aRecycledDimensionIdDoesNotInheritTheOldBodysName() { assertTrue("the new body's name must lie in ITS system's neighbourhood", reg.anchorForCell(secondName.get()).isPresent()); assertEquals("...which is its own star's anchor", - GalacticCoord.ofSectorLocal(4000, 0, 0, 0, 0, 0), + GalacticCoord.ofSectorLocal(ANOTHER_SUPER_CELL, 0, 0, 0, 0, 0), reg.anchorForCell(secondName.get()).get()); } @@ -615,7 +622,7 @@ public void aRecordedNameThatLeftItsSystemsBoxIsReDerivedRatherThanServed() { // The star is re-placed a long way off — an XML edit, a re-authored layout. The recorded name // is now nowhere near the system it belongs to. - GalacticCoord newAnchor = GalacticCoord.ofSectorLocal(9000, 0, 0, 0, 0, 0); + GalacticCoord newAnchor = GalacticCoord.ofSectorLocal(3L * ANOTHER_SUPER_CELL, 0, 0, 0, 0, 0); reg.place(newAnchor, 6004); Optional served = reg.coordForPlanet(body); @@ -734,7 +741,7 @@ public void theSkyFeedUnionsTheSystemWithTheObserversOwnCell() { public void interstellarVoidIsFedNothing() { UniverseRegistry reg = new UniverseRegistry(); reg.place(GalacticCoord.ORIGIN, 6008); - GalacticCoord farAway = GalacticCoord.ofSectorLocal(500_000, 0, 0, 0, 0, 0); + GalacticCoord farAway = GalacticCoord.ofSectorLocal(ANOTHER_SUPER_CELL, 0, 0, 0, 0, 0); assertFalse("the fixture's cell must belong to no system", reg.anchorForCell(farAway).isPresent()); assertTrue("the space between stars is black", reg.skyBodiesAt(farAway).isEmpty()); From 663d46142c47e984e275e718e9a28bb80ec737ea Mon Sep 17 00:00:00 2001 From: StannisMod Date: Fri, 14 Aug 2026 15:52:23 +0300 Subject: [PATCH 13/42] docs: rewrite the planetDefs reference around its semantics - units, the chart scale, and the file's read/rewrite lifecycle - what minSpacing bounds and what it does not - companion orbits, and changing a generator parameter mid-save - a combinations section: what wins when two fields disagree - retarget the texture pointer at this repository --- README.md | 4 +- docs/README_PLANETDEFS.md | 1840 +++++++++---------------------------- 2 files changed, 457 insertions(+), 1387 deletions(-) diff --git a/README.md b/README.md index 9e5138c97..ac3fa9e5c 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,11 @@ It is not released, and while it is in development: The mod adds its own dimensions, worldgen and ore-processing chain, so it takes up room in a pack. Each major system has a config switch, including worldgen, planet weather and the whole 3.0.0 space subsystem. -Planets, stars and ores are configured in XML, with references and templates in [`docs/`](docs/): +Planets, stars, the procedural galaxy and ores are configured in XML, with references and templates +in [`docs/`](docs/): - planetDefs — [reference](docs/README_PLANETDEFS.md) · [template](docs/TEMPLATE_planetdefs.xml) + — every element and attribute, its unit, and what wins when two of them disagree - oreConfig — [reference](docs/README_ORECONFIG.md) · [template](docs/TEMPLATE_oreconfig.xml) Coming from an older 2.x build: commands moved into subcommands, so **command scripts and quest-book command diff --git a/docs/README_PLANETDEFS.md b/docs/README_PLANETDEFS.md index f01b10e41..5afda1ad1 100644 --- a/docs/README_PLANETDEFS.md +++ b/docs/README_PLANETDEFS.md @@ -1,297 +1,282 @@ -# Advanced Rocketry `planetDefs.xml` Reference +# `planetDefs.xml` — the universe catalogue -This document explains how `planetDefs.xml` is structured and which tags and attributes are supported. +Everything Advanced Rocketry lets a pack author say about stars, planets and the procedural galaxy. +This file documents the format exhaustively: every element, every attribute, its unit, its default, +what happens when it is missing or malformed, and — the part that costs people days — what happens +when two of them are stated together. -Place the file at: - -`config/advancedRocketry/planetDefs.xml` - - -**Template** found here [`TEMPLATE_planetdefs.xml`](TEMPLATE_planetdefs.xml) +--- +## 1. Where the file is, and when it is read and written -This reference tries to document all fields that are loaded from planetdefs. +| | path | +|---|---| +| **template** (what a pack ships) | `config/advancedRocketry/planetDefs.xml` | +| **live copy** (what the game reads) | `/advRocketry/planetDefs.xml` | ---- +1. On world load the game looks for the **live copy**. If it is absent, the **template** is copied + there and that copy is loaded. +2. The config option `resetPlanetsFromXML` (section `Planet` of `advancedRocketry.cfg`) forces the + copy to happen again, overwriting the live copy from the template. That is the only supported way + to push a template edit into an existing world. It **resets itself to `false` after one load** + unless `ResetOnlyOnce` is set to `false`, which is what a pack developer wants while iterating. +3. **On every world save the live copy is REWRITTEN** from the in-memory model. -## 1. Purpose +Consequence of (3), and it surprises everyone exactly once: -`planetDefs.xml` lets you define stars, planets, moons, and planet-specific configuration manually. +- **Comments are lost.** The writer builds a new document; nothing in the file survives that the + reader did not turn into model state. +- **Unknown elements and attributes are lost**, because they were never read (see §2). +- **`numPlanets` / `numGasGiants` are written back as `0`.** Random planets are generated once, at + first load, and become ordinary `` entries. They are not regenerated on later loads. +- **A companion star loses its `name`.** The writer does not emit `name` for a nested ``; it is + regenerated as `-`. -Place the file as: +So: edit the **template**, not the live copy, and keep the template under version control. -`config/advancedRocketry/planetDefs.xml` +--- -This document is intended as a reference-first replacement for the old XML readme. +## 2. Parsing rules that apply everywhere + +- **The root element must be ``.** No root, or unparseable XML → the world fails to load with + a crash report naming the file. That is deliberate: a silently half-loaded catalogue is worse. +- **Anything unrecognised is ignored silently.** A misspelled element or attribute produces no + warning at all. Check your spelling; the game will not. +- **A malformed `` is skipped, not fatal.** The rest of the catalogue loads and the reason is + printed to the log. The guard sits at the top-level planet, so a malformed MOON takes its parent + planet and that planet's other moons down with it — not the whole file. +- **A malformed number inside a recognised element is warned about and the field keeps its default**, + unless stated otherwise below. +- **Booleans are `true` / `false`**, case-insensitive. Anything else reads as `false`. +- **Element ORDER never matters.** Attribute order never matters. +- **Colours** accept either three comma-separated floats in `0..1` (`0.5,0.5,1.0`) or one + `0x`-prefixed hex triple (`0xRRGGBB`). Anything else warns and keeps the default. --- -## 2. Basic File Structure +## 3. Units — read this before anything else + +| quantity | unit | notes | +|---|---|---| +| **orbital distance** | `100` = 1 AU | Same unit for a planet round its star and for a companion star round its primary. | +| **orbital angle** | DEGREES | `orbitalTheta` on a planet and on a companion alike. | +| **orbital inclination** | DEGREES | `orbitalPhi`. Tilts the orbit; it does not enlarge it. | +| **star temperature** | `100` = Sol | Multiply by 58 for Kelvin. | +| **star size** | solar radii | `1.0` = Sol. | +| **planet mass** | Earth masses | | +| **planet radius** | Earth radii | | +| **surface gravity** | percent of Earth's | `100` = 1 g. Clamped to `0..400`. | +| **atmosphere density** | `100` = 1 atm | Clamped to `0..1600`. | +| **planet temperature** | KELVIN | Computed, not authored — see `avgTemperature` in §7. | +| **rotational period** | ticks | `24000` = one Minecraft day. Must be `> 0`. | +| **star map position** | arbitrary map units | `x` / `y` on ``; affects the star-selector GUI only. | +| **galactic anchor** | cell indices | `"sectorX,sectorY,sectorZ"`. One cell is 4 000 000 blocks. | + +**The chart scale.** One orbital-distance unit is **5 983 914 blocks**, i.e. one AU is +149 597 870 700 m at 250 m per block. This is the one law that turns an orbit into a place, and it is +the same for authored and procedural systems. Every derived number — insolation, equilibrium +temperature, orbital period, flight time — comes from the orbital distance, so a body's stated +distance and where a ship actually finds it are the same statement. -### Root structure +--- -The root element is: +## 4. Document structure ```xml -``` + + + -A galaxy contains one or more `` entries. + -A `` can contain: -- one or more `` entries -- one or more nested `` entries (sub-stars / multi-star systems) - -A `` can contain: -- property tags such as ``, ``, etc. -- nested `` entries, which are treated as moons / child bodies - -### 2.1 Basic examples - -```xml - - - - ... - - - -``` -```xml - - - - ... - - + + + + + + ``` ---- - -## 3. Rules and Conventions - -### 3.1 Nesting rules - -- A `` inside a `` defines a planet orbiting that star. -- A `` inside another `` defines a moon / child body. -- A `` inside another `` defines a sub-star. - -### 3.2 Parser behavior - -The loader is tolerant in some places and strict in others. - -Examples: -- Some numeric fields are clamped -- Some invalid values are ignored with warnings -- Some fields use direct `Integer.parseInt(...)` without a `try/catch`; malformed values there may break loading - -### 3.3 Scope of this document - -This document intentionally excludes fields that are only exported/written but not meaningfully loaded from XML. - -Example: -- `avgTemperature` is written by XML export code, but it is not a meaningful author-controlled XML input because temperature is recomputed after load +Only ``, `` and `` are recognised directly under ``. --- -## 4. Star Reference - -### 4.1 `` overview - -Defines a star system entry. - -A top-level `` may contain: -- planets -- sub-stars - -A nested `` is treated as a sub-star. - -### 4.2 `` attributes - -#### `name` -Display name of the star. - -```xml - -``` - -#### `temp` -Star temperature integer. - -```xml - -``` - -Notes: -- Parsed as an integer -- If malformed, the loader falls back to `100` for sub-star parsing - -#### `x` -Galaxy map X position. - -```xml - -``` - -#### `y` -Galaxy map Y position. - -```xml - -``` - -Notes: -- Internally this is used as the star's Z/map Y position - -#### `size` -Star size multiplier. - -```xml - -``` - -Notes: -- Parsed as float - -#### `numPlanets` -Maximum number of randomly generated planets for the star. - -```xml - -``` - -#### `numGasGiants` -Maximum number of randomly generated gas giants for the star. +## 5. `` — the procedural galaxy -```xml - -``` - -Notes: -- These values apply to random planet generation for the star -- Manually defined `` entries can still be added regardless -- For a fully manual system with no extra random planets, use `numPlanets="0"` and `numGasGiants="0"` - -#### `blackHole` -Marks the star as a black hole. - -```xml - -``` +Present → procedural systems exist alongside the authored ones. Absent → the universe holds only what +this file names. -Accepted values: -- `true` -- `false` +| attribute | unit | default | meaning | +|---|---|---|---| +| `density` | 0..1 | `0.35` | Chance that a given cube of space holds a system, before the void mask. Clamped; `NaN` reads as `0`. | +| `minSpacing` | cells | `40018890` | Edge of the cube that holds **at most one** system, i.e. how far apart stars stand. The default is 4.23 light years. Floors at 1. | +| `clusterScale` | super-cells | `16` | Resolution of the coarse field that separates populated space from void. Floors at 1. | +| `voidFraction` | 0..1 | `0.6` | Fraction of space that is empty. `1.0` yields an empty galaxy. Clamped; `NaN` reads as `0`. | -#### `diskAngle` -Black hole disk angle / star disk angle. +### `` — the archetype table -```xml - -``` - -Notes: -- Parsed as float - -#### `separation` -Only meaningful on nested `` entries. - -```xml - -``` +Zero `` children → the built-in table stands. One or more → they **replace** it entirely. -Notes: -- Parsed as float -- Used for sub-star separation in multi-star systems +| attribute | unit | default | meaning | +|---|---|---|---| +| `temp` | `100` = Sol | `100` | Temperature, and therefore colour. | +| `minSize` / `maxSize` | solar radii | `0.8` / `1.2` | Size range. `minSize` floors at `0.1`; `maxSize` is raised to `minSize` if smaller. | +| `weight` | relative | `1` | Draw weight. Floors at `1`. Weights are summed in 64-bit, so extreme values do not collapse the distribution. | -### 4.3 Star examples +### What `minSpacing` does and does not do -#### Single star +**It moves the STARS apart and nothing else.** It does not decide how large a system is: a system's +extent follows its outermost orbit. Raising it does not inflate a single planet's orbit; lowering it +does not squash one. -```xml - - ... - -``` +What it does bound is **how much room a system has**. Every system is guaranteed a clear space of +**10 000 AU** around its star — no two stars ever stand closer than that — and its named bodies +(planets, moons, belts) stay inside **5 000 AU**, half of that clear space, which is what keeps two +systems' neighbourhoods from overlapping. -#### Binary star +**A system that does not fit loses BODIES, never scale.** A world drawn past its system's room is +dropped; the worlds that remain stand exactly where their own orbits say. This is not a corner +anybody meets at the shipped numbers: the widest zone any built-in star archetype can draw is 569 AU +against 5 000 AU of room, a factor of nearly nine. It becomes reachable only if `minSpacing` is cut by +more than two orders of magnitude — below roughly 170 000 cells systems start losing outer worlds, +and below about 8 cells only the star survives. -```xml - - - ... - -``` +### Changing a `` parameter mid-save is UNDEFINED -#### Black hole +`density`, `minSpacing`, `clusterScale` and `voidFraction` are inputs to a **derived** universe: +nothing about a procedural system is stored, so changing any of them relocates every star, every +planet and every generated name. **You get a different universe, and anything a player recorded about +the old one — coordinates, memory crystals, a route — points at nothing.** -```xml - - ... - -``` +There is no migration and there cannot be one: there is no old universe on disk to migrate. If you +change these, start a new world. --- -## 5 Planet Reference - -### 5.1 `` overview - -Defines a planet or moon. - -- A `` directly inside a `` is a planet. -- A `` inside another `` is a moon / child body. -- A `` could also be defined as `` - - GasGiants: - - Has no surface to land on - - Intended for Gas Collection or cosmetics - -### 5.2 `` attributes - -#### `name` -Planet name. - -```xml - -``` - -#### `DIMID` -Explicit dimension ID. - -```xml - -``` -Note: -- Case sensitive, canonical "DIMID" -#### `dimMapping` -Makes a planet out of a non-native dimension. - -```xml - -``` -The presence of the attribute is what matters. - -Notes: -- This should be paired with a correct `DIMID` -- AR will not enforce weather non-native dimension (2.2.3+) -- As with note above not all entries might apply to other mods dimensions. - -#### 5.3 `customIcon` -Planet icon basename. - -```xml - -``` +## 6. `` — the type table for procedural worlds + +Present → **replaces** the built-in preset table wholesale. Absent → the built-in table stands. +Types are what a procedurally derived world is classified as, after its physics is computed; they are +never applied to an authored ``. + +```xml + + + + + + + + + + advancedrocketry:moondark;10,minecraft:ice_flats;30 + 0 + minecraft:water + + +``` + +| attribute on `` | default | meaning | +|---|---|---| +| `name` | `""` | Identifier, shown in scans. | +| `weight` | `10` | Draw weight among the types that ADMIT a given world. | +| `gasGiant` | `false` | This type has no surface. | +| `allowsOxygen` | `false` | Worlds of this type may roll breathable air. Only ~18 % of those that may, do. | +| `tidallyLockable` | `true` | Worlds of this type can keep one face to their star. | + +| child | attributes | default range | meaning | +|---|---|---|---| +| `` | `min`, `max` | `0..1600` | Atmosphere density band this type admits. | +| `` | `min`, `max` | `0..5000` | Kelvin band. | +| `` | `min`, `max` | `0..400` | Percent-of-Earth band. | +| `` | — | — | Container for `` options; one is drawn by weight. | +| `` | — | — | Biome palette, same format as a planet's (§7). | +| `` | — | unset | Sea level for worlds of this type. | +| `` | — | unset | Registry name of the liquid. | +| `` | — | — | Ore table, same format as a planet's (§8). | + +A world must satisfy **all three** ranges to be admitted by a type. Every attribute has a default, so +`` is valid and matches nearly everything — which makes it a very greedy entry. + +### `` — one terrain option + +| attribute | applies to | meaning | +|---|---|---| +| `source` | all | `NATIVE`, `MOD_WORLDTYPE` or `TEMPLATE`. Unknown names fall back to `NATIVE`. | +| `worldType` | `MOD_WORLDTYPE` | The world-type name another mod registered. | +| `path` | `TEMPLATE` | Template identifier. | +| `genType` | `NATIVE` | Built-in generator variant. | +| `options` | `MOD_WORLDTYPE` | Generator settings string, passed through verbatim — **not trimmed**, because whitespace can be significant to the receiving generator. | +| `weight` | all | Draw weight among this type's options. Default `1`. | + +**A `MOD_WORLDTYPE` option whose mod is not installed is dropped BEFORE the draw**, and its weight is +redistributed among the remaining options. A type all of whose options are unavailable falls back to +`NATIVE`. This is why a type should always carry at least one `NATIVE` option. +--- -## Built-in `customIcon` values +## 7. `` and `` + +### `` attributes + +| attribute | unit | required | meaning | +|---|---|---|---| +| `name` | — | no | Display name. | +| `temp` | `100` = Sol | no (default `100`) | Temperature; drives colour and luminosity. A malformed value warns and falls back to `100`. | +| `size` | solar radii | no (default `1.0`) | Radius. | +| `x`, `y` | map units | no | Position on the star-selector map. `y` is the map's Z. | +| `galacticCoord` | `"sx,sy,sz"` | no | Explicit anchor cell. Malformed → warns and uses the origin. Absent → a deterministic fallback cell is assigned. | +| `numPlanets` | count | **yes** | How many random planets to generate for this star at FIRST load. Missing → warning and none. | +| `numGasGiants` | count | **yes** | The same for gas giants. | +| `blackHole` | boolean | no | This star is a black hole: a quarter of the light its size and temperature would otherwise give. | +| `diskAngle` | degrees | no (default `70`) | Accretion-disc tilt, render only. | + +`numPlanets` / `numGasGiants` fire **once**, at the first load of a world. They are written back as +`0`, so the generated planets become ordinary entries and are not regenerated. Hand-written +`` children are additional to them, not instead of them. + +### A nested `` is a COMPANION + +| attribute | unit | default | meaning | +|---|---|---|---| +| `name` | — | `-` | Display name. **Not written back** on save. | +| `temp` | `100` = Sol | `100` | | +| `size` | solar radii | `1.0` | | +| `orbitalDistance` | `100` = 1 AU | `5` (0.05 AU) | How far this star orbits its primary. | +| `orbitalTheta` | degrees | spread automatically | Its angle on that orbit. Companions with no stated angle are spread apart rather than stacked. | +| `blackHole`, `diskAngle` | — | — | As above. | + +Companions nest: a companion may itself carry companions, and the geometry composes. Consequences +that are easy to miss: + +- **A companion is a star with its own identity.** It gets its own star id, so a `` can be + bound to it and a world can orbit the companion rather than the primary. +- **Every star of a system lights every world in it.** Illumination is the sum of the flux each star + delivers at its own distance, so a close pair nearly doubles a world's light and a companion 20 AU + out adds only a little. This feeds temperature, solar panels and every derived climate number. +- **A companion's apparent place in the sky follows from its distance**, not from a fixed tilt: a + close pair reads as two suns almost together, a wide one puts its companion elsewhere in the sky. +- `orbitalDistance` on a companion is the SAME unit as on a planet. It used to be an angle called + `separation`; that attribute no longer exists and is ignored if present. + +### `` attributes + +| attribute | meaning | +|---|---| +| `name` | Display name. | +| `DIMID` | Explicit dimension id. Absent → the next free id is assigned. Malformed → **the whole planet is skipped**. | +| `dimMapping` | Presence alone (any value, including empty) marks this as a dimension another mod owns; Advanced Rocketry decorates it instead of creating it. | +| `customIcon` | Basename of the planet-selector texture. See the catalogue below. | + +### Built-in `customIcon` values Built-in planet icon basenames: `src/main/resources/assets/advancedrocketry/textures/planets/` -### Standard icons +#### Standard icons @@ -358,7 +343,7 @@ Built-in planet icon basenames:
-### Additional normal-only textures +#### Additional normal-only textures @@ -381,11 +366,11 @@ Built-in planet icon basenames:
-### Special case +#### Special case - `customIcon="void"` is handled specially in the system map and renders the body at size `0`. -### 5.3.1 Adding your own `customIcon` +#### Adding your own `customIcon` Resource pack should provide: @@ -404,1218 +389,301 @@ Notes: - The value is lowercased during lookup - Custom icons are loaded as `.png` for the normal planet texture and `leo.jpg` for the LEO/orbit texture. - The LEO texture is used for orbit views -- Built-in examples can be found in the mod resources under: - https://github.com/kaduvill/AdvancedRocketry/tree/1.12/src/main/resources/assets/advancedrocketry/textures/planets - - ---- - -## 6. Planet Property Tags - -### 6.1 Visual and sky settings - -#### `` -Planet fog color. - -Accepted formats: -- comma-separated floats: `r,g,b` -- hex prefixed with `0x` - -Examples: - -```xml -0.5,0.2,1 -or -0x87FFFF -``` - -Notes: -- RGB float components are expected in the range `0` to `1` -- Hex is parsed as an integer after removing the `0x` prefix - -#### `` -Planet sky color. - -Accepted formats: -- comma-separated floats: `r,g,b` -- hex prefixed with `0x` - -Examples: +- Every built-in texture lives in this repository under + [`src/main/resources/assets/advancedrocketry/textures/planets/`](../src/main/resources/assets/advancedrocketry/textures/planets/). + +A `` nested inside a `` is a **moon** of it. Moons nest arbitrarily deep. A moon's +`orbitalDistance` is measured from its PARENT, not from the star. + +### `` child elements + +Physical: + +| element | unit | notes | +|---|---|---| +| `orbitalDistance` | `100` = 1 AU | Clamped to `1 .. Integer.MAX_VALUE`. | +| `orbitalTheta` | degrees | Angle at time zero. Fractional degrees are kept. | +| `orbitalPhi` | degrees | Inclination. Taken modulo 360. | +| `retrograde` | boolean | Orbits the other way. | +| `rotationalPeriod` | ticks | Must be `> 0`; a non-positive value warns and is ignored. | +| `tidallyLocked` | boolean | Keeps one face to its star; overrides `rotationalPeriod` in effect. | +| `mass` | Earth masses | See the precedence rule below. | +| `radius` | Earth radii | See the precedence rule below. | +| `gravitationalMultiplier` | percent of Earth | Clamped to `0..400`. See below. | +| `atmosphereDensity` | `100` = 1 atm | Clamped to `0..1600`. | +| `hasOxygen` | boolean | Default `true`. Only `false` is written back. | +| `metallicity` | relative to Sol | Feeds ore richness. `1.0` is not written back. | +| `avgTemperature` | Kelvin | **Written, never read.** The temperature is recomputed at load from the star, the orbital distance and the atmosphere. Editing it does nothing. | -```xml -0.3,0.6,1 -or -0x4C99FF -``` - -#### `` -Controls color override behavior for sky/fog rendering. - -```xml -true -``` - -Accepted values: -- `true` -- `false` - -Notes: -- Used by world provider sky/fog color calculation - -#### `` -Overrides AR's custom sky renderer for that world. - -```xml -true -``` - -Accepted values: -- `true` -- `false` - -Notes: -- This tag only disables AR's custom planet sky for this planet -- Also affected by the global client config option `planetSkyOverride` - - If `planetSkyOverride=false` in the config, AR's custom planet sky is already disabled globally and this tag has no additional effect - -#### `` -Controls planet decoration rendering override. - -```xml -false -``` - -Accepted values: -- `true` -- `false` - -Notes: -- Overrides whether decorators such as shadows / atmosphere-style planet rendering details should be shown - -### 6.2 Atmosphere, gravity, orbit, and rotation - -#### `` - -Atmosphere density / pressure value. - -Example: - - 100 - -Meaning: -- `100` is Earthlike. -- Clamped to `[0 - 1600]` -- Atmosphere pressure category is selected with strict `>` thresholds: - - `0–25`: no atmosphere / vacuum - - `26–75`: low atmosphere / low oxygen pressure - - `76–200`: normal pressure (Breathable) - - `201–800`: high pressure - - `801–1600`: super-high pressure -- Temperature can still override the result into hot or superheated atmosphere types. - -Notes: -- World provider uses atmosphere density for rain/snow/ice behavior and cloud rendering. - -#### `` - -Used to disable `breathable` for normal pressure planets - -Example: - - true - -Accepted values: -- `true` -- `false` - -Default: -- `true` if omitted. - -Meaning: -- This tag is mainly useful for disabling oxygen on breathable planets. -- If the planet has no atmosphere, this tag has no practical breathing effect. - -#### `` -Gravity value, using `100 = Earthlike`. - -```xml -100 -``` - -Meaning: -- `100` = `1.0` -- `50` = `0.5` -- `150` = `1.5` - -Loader clamp: -- Min XML value: `0` -- Max XML value: `400` - -Internal conversion: -- Stored as `value / 100f` - -Notes: -- World provider uses this value directly for planetary gravity queries - -#### `` -Distance from the parent body. - -```xml -100 -``` - -Meaning: -- For planets, this is distance from the star -- For moons, this is distance from the parent planet - -Loader clamp: -- Min: `1` -- Max: `2147483647` - -Notes: -- For planets orbiting stars, this affects temperature -- For moons, code uses parent-star distance for solar temperature - -#### `` -Starting angular displacement in degrees. - -```xml -180 -``` - -Notes: -- Parsed as integer degrees -- Converted internally to radians -- The parser stores the value modulo `360` - -#### `` -Orbital plane angle in degrees. - -```xml -90 -``` - -Notes: -- Parsed as integer -- Stored modulo `360` - -#### `` -Whether the body orbits in retrograde. - -```xml -true -``` - -Accepted values: -- `true` -- `false` +Appearance: -#### `` -Length of the day/night cycle in ticks. +| element | notes | +|---|---| +| `fogColor`, `skyColor`, `ringColor` | Colour, see §2. | +| `hasRings` | boolean. | +| `ringAngle` | degrees. | +| `hasShading` | boolean; whether the world is decorated with shading. | +| `hasColorOverride` | boolean. | +| `skyRenderOverride` | boolean. | +| `customIcon` | attribute, not element — see above. | -```xml -24000 -``` - -Meaning: -- `24000` ticks = 20 minutes - -Loader rule: -- Must be greater than `0` +World generation: -Notes: -- Used by `WorldProviderPlanet.calculateCelestialAngle()` +| element | notes | +|---|---| +| `genType` | Built-in generator variant. Only written when non-zero. | +| `terrainSource` | `NATIVE`, `MOD_WORLDTYPE` or `TEMPLATE`. Unknown → `NATIVE`. Only written when not `NATIVE`. | +| `terrainWorldType` | Name of another mod's world type. | +| `terrainTemplate` | Template identifier. | +| `terrainGeneratorOptions` | Passed through verbatim, **not trimmed**. | +| `seaLevel` | Block height. | +| `orbitHeight` | Block height at which a rocket leaves this world. Only written when overridden. | +| `oceanBlock` | Registry name. An unknown block warns and yields air. | +| `fillerBlock` | `mod:block` or `mod:block:meta`. Fewer than two parts warns and is ignored. | +| `forceRiverGeneration` | boolean. | +| `biomeIds` | See the format below. | +| `craterBiomeWeights` | See the format below. | +| `generateCraters`, `generateCaves`, `generateVolcanos`, `generateStructures`, `generateGeodes` | boolean. An empty value leaves the default. **Each is also a global config switch, and the global `false` wins.** | +| `craterFrequencyMultiplier`, `volcanoFrequencyMultiplier`, `geodeFrequencyMultiplier` | float. Only written when not `1` and when the matching feature is enabled. | +| `oreGen` | See §8. | +| `laserDrillOres` | See the format below. **Ignored entirely on a gas giant.** | +| `geodeOres`, `craterOres` | Comma-separated ore-dictionary names. Unknown names are dropped silently. | -#### `` -Sea level value. +Content and progression: -```xml -63 -``` +| element | notes | +|---|---| +| `GasGiant` | boolean, spelled with capitals. A gas giant has **no surface**: it cannot be landed on and is not offered as a descent target. | +| `gas` | Fluid name; a harvestable gas. Repeatable. Read on any planet but written back only for a gas giant, so a `` on a rocky world is lost at the first save. Unknown fluid warns and is skipped. | +| `isKnown` | boolean. **Writes into a GLOBAL list**, not into the planet: it marks this dimension as known to every player from the start. | +| `artifact` | An item stack required to unlock travel here. Repeatable. | +| `spawnable` | An entity that spawns here. See below. | -Notes: -- Runtime setter clamps to `0..255` +Weather: +| element | unit | notes | +|---|---|---| +| `rainStartLength`, `rainProlongationLength` | ticks | A malformed value throws and skips the whole planet — these are the only numeric fields without a `try`. | +| `thunderStartLength`, `thunderProlongationLength` | ticks | Same. | +| `rainMarker`, `thunderMarker` | ticks | Same. | +| `acidicRain` | boolean | Rain damages an unprotected player. | -#### `` -Controls the `hasRivers` flag. +### `` — mob spawns ```xml -true +minecraft:zombie ``` -Accepted values: -- `true` -- `false` +The text content is a registry name (`minecraft:zombie`) or, failing that, a fully-qualified entity +class name. Neither resolving → a warning, and the entry is skipped. -Notes: -- This sets `properties.hasRivers` -- The final `hasRivers()` runtime behavior may also depend on atmosphere and temperature if this is not explicitly forced - -### 7.3 Rings and gas giants - -#### `` -Whether the body has rings. - -```xml -true -``` +| attribute | default | notes | +|---|---|---| +| `weight` | `100` | Spawn weight. Floors at 1. | +| `groupMin` | `1` | Floors at 1. | +| `groupMax` | `1` | Floors at 1; raised to `groupMin` if smaller. | +| `nbt` | — | JSON NBT applied to the spawned entity. Invalid JSON or NBT logs a loud configuration error and the entity spawns without it. | -Accepted values: -- `true` -- `false` +### Biome list formats -#### `` -Ring angle integer. +`biomeIds` — comma-separated `biome` or `biome;weight`: ```xml -70 +minecraft:desert;40,advancedrocketry:moondark;10 ``` -Notes: -- XML loader uses direct `Integer.parseInt(...)` here -- Use a valid integer - -#### `` -Ring color. +- `biome` is a registry name (preferred) or a raw numeric id (legacy, and dependent on the installed + mod set). +- `weight` defaults to `30`. A weight of `0` warns and reverts to `30`. +- A malformed entry warns and is skipped; the rest of the list still applies. +- **An empty or absent list is not an empty palette**: a planet with no biomes is given every biome + its climate admits. -Accepted formats: -- comma-separated floats: `r,g,b` -- hex prefixed with `0x` +`craterBiomeWeights` — the same shape, but the weight is a crater frequency and defaults to `100`, +and a missing `;weight` term warns. Numeric ids are **not** accepted here; only registry names. -```xml -0.4,0.4,0.7 -``` +### `laserDrillOres` format -#### `` -Marks the body as a gas giant. +Comma-separated entries, each `oreName` or `oreName;count` or `itemName;count;meta`: ```xml -true +oreIron;2,oreGold;1,minecraft:diamond;1;0 ``` -Accepted values: -- `true` -- `false` +An ore-dictionary name that exists but has no registered items — the providing mod is not installed — +warns and is skipped. A name that is neither an ore-dictionary entry nor an item id warns and is +skipped. The raw string is stored and written back verbatim, so entries for absent mods survive a +round-trip. -Notes: -- Intended for use with gas giants and gas missions -- Canonically saved/exported as `GasGiant` +### `artifact` syntax -#### `` -Adds a harvestable gas/fluid name. +An item stack a player must hold to be allowed to travel here. Format `item_or_block meta count`, +space separated: ```xml -hydrogen -helium +minecraft:diamond 0 1 ``` -Notes: -- The value must resolve through the fluid registry -- Intended for use with gas giants and gas missions - -### 6.4 Biomes +`meta` defaults to `0` and `count` to `1`. Repeat the element for several artifacts; an unresolvable +item yields an empty stack and is skipped. -#### `` -Biome list for the planet. Overrides the automatic biome-selection - -Accepted entry formats: -- numeric biome ID -- biome resource location -- weighted biome entry using `biome;weight` +--- -Examples: +## 8. `` — ore generation ```xml -0,12 -minecraft:plains,minecraft:forest -minecraft:plains;30,biomesoplenty:alps;15 + + + ``` -Notes: -- If a weight is omitted or `0`, default weight is `30` -- Resource locations are preferred over old numeric IDs -- If `` is omitted, the planet falls back to automatic biome selection - - Automatic biome selection is affected by global biome-related config and biome lists, including logic such as blacklist handling and `maxBiomesPerPlanet` -- If `` is provided, the loader uses that explicit biome list instead of automatic biome selection - -#### `` -Controls which biomes can be used as crater origin biomes, and how likely craters are to generate in each biome. +Only `` children are read; anything else under `` is ignored. -Accepted format: -- Comma-separated entries -- Each entry uses `biome;weight` -- +| attribute | required | clamp | meaning | +|---|---|---|---| +| `block` | **yes** | — | Registry name. Missing → the entry is skipped with a warning. | +| `meta` | no (default `0`) | — | Block metadata. Malformed → the entry is skipped. | +| `minHeight` | **yes** | floors at 1 | Missing or malformed → the entry is skipped. | +| `maxHeight` | **yes** | `minHeight..255` | Missing or malformed → the entry is skipped. | +| `clumpSize` | **yes** | `1..255` | Blocks per vein. Missing or malformed → the entry is skipped. | +| `chancePerChunk` | **yes** | `1..255` | Veins attempted per chunk. Missing or malformed → the entry is skipped. | -Example: +Every clamp is silent. A `clumpSize` of `1000` becomes `255` with no warning. -```xml -minecraft:desert;100,minecraft:mesa;60 -``` - - Behavior: +--- -- If `` is omitted or empty, craters may originate in any biome. -- If present, only listed biomes are valid crater origin biomes. -- The weight is a percentage-like chance from `0` to `100`. - - `100` = crater origins in this biome are always allowed when the generator attempts one. - - `50` = about half of crater origin attempts in this biome are allowed. - - `1` = very rare crater origin attempts in this biome. - - `0` = effectively disables crater origins in this biome. -- The biome check is done at the crater origin chunk, not every block touched by the crater. - - Large craters may still extend into neighboring biomes. -- If frequency is omitted, the loader warns and defaults that biome weight to `100`. -- Invalid biome resource locations are ignored with a warning. +## 9. Combinations — what wins when two fields disagree -Notes: +**Gravity versus bulk.** A planet may state `gravitationalMultiplier`, or `mass` **and** `radius`, or +all three. -- The loader expects biome resource locations such as `minecraft:desert` or `biomesoplenty:volcanic_island`. -- This setting controls where craters may originate; it does not change crater shape, size, block palette, or crater ores. -- Crater generation must still be enabled by both `true` and the global `generateCraters` config option. -- Actual crater generation also depends on atmosphere conditions. +| stated | result | +|---|---| +| `gravitationalMultiplier` only | That gravity. No mass or radius; anything needing bulk falls back to gravity. | +| `mass` + `radius` only | Gravity is **derived**: `g = M / R²`, clamped to `0.05 .. 4.0` g. | +| all three | **The authored gravity wins.** Mass and radius are still stored and still used for orbital periods and for anything that needs a real bulk. | -### 6.5 Generation type and worldgen switches +The last row is the important one: adding `mass` and `radius` to a planet that already states a +gravity cannot change how that planet plays. It only gives the model the numbers it was missing. -#### `` -Generation type integer. +**Mass and radius are order-independent** but each is applied against the other's current value, so +stating only one of them leaves the other at zero — and a zero radius means no bulk properties at all. +State both or neither. -```xml -1 -``` +**Gas giant versus surface.** `true` makes the world surfaceless. It is then not +a landing target however else it is configured, `laserDrillOres` on it is ignored, and only `` +entries can be harvested from it. -- `0` or omitted: - - normal planet generation -- `1`: - - cave planet generation (based on vanilla nether) - -- `2`: - - Asteroid-belt world +**Tidal locking versus rotation.** `tidallyLocked` makes the world's rotation equal its orbit. A +`rotationalPeriod` stated alongside it is stored but has no visible effect. -#### `` -Enable/disable crater generation. +**`orbitalDistance` versus everything derived.** Insolation, equilibrium temperature, orbital period, +climate and the physical distance a ship flies all come from this one number. `avgTemperature` is +recomputed from it at every load — you cannot author a temperature that contradicts an orbit. -```xml -true -``` +**Star temperature and size versus planet climate.** Changing a star's `temp` or `size` re-derives the +climate of every world around it on the next load, because temperature is computed and not stored. -Accepted values: -- `true` -- `false` +**`DIMID` versus automatic ids.** Stating `DIMID` on some planets and not others is supported; the +automatic allocator skips ids already taken. Two planets stating the SAME `DIMID` is not detected — +the second silently replaces the first. +**`dimMapping` versus everything physical.** A mapped dimension is generated by whoever owns it. +Terrain elements on it are ignored; climate, gravity and atmosphere still apply. -Notes: -- This flag is also gated by the global config option `generateCraters` - - If the global config is `false`, crater generation is disabled globally regardless of this XML value - - If the global config is `true`, this tag can still disable craters for an individual planet -- Actual crater generation also depends on atmospheric conditions +**Global config switches versus per-planet flags.** `generateCraters`, `generateGeodes`, +`generateVolcanos` and `generateStructures` exist both here and in the mod config. **The global +`false` overrides a per-planet `true`.** The reverse is not true: a global `true` does not force a +planet that declined. -#### `` -Enable/disable geode generation. +**`` versus ``.** Types classify PROCEDURAL worlds only. They never modify an +authored ``, however well its numbers match a type's ranges. -```xml -true -``` +**`` versus authored stars.** They coexist. An authored star occupies its anchor cell and +owns that whole neighbourhood; the procedural generator fills what is left. Two authored anchors in +one neighbourhood is a configuration error and is reported. -Accepted values: -- `true` -- `false` +--- -Notes: -- This flag is also gated by the global config option `generateGeodes` - - If the global config is `false`, geode generation is disabled globally regardless of this XML value - - If the global config is `true`, this tag can still disable geodes for an individual planet +## 10. Worked minimal examples -#### `` -Enable/disable volcano generation. +A single authored system, no procedural galaxy: ```xml -true + + + + 100 + 0 + 100 + 100 + true + + 30 + 16 + 0 + false + + + + ``` -Accepted values: -- `true` -- `false` - -Notes: -- Canonical spelling is `generateVolcanos` -- This flag is also gated by the global config option `generateVolcanos` - - If the global config is `false`, volcano generation is disabled globally regardless of this XML value - - If the global config is `true`, this tag can still disable volcanos for an individual planet - -#### `` -Enable/disable structure generation. +A wide binary whose companion carries a world of its own: ```xml -true + + + + 120 + 1.0 + 1.0 + + ``` -Accepted values: -- `true` -- `false` +`Alpha I` is lit by both stars, with `Beta`'s contribution falling off over its own 23 AU. -Notes: -- This flag is also gated by the global config option `generateVanillaStructures` - - If the global config is `false`, vanilla/map-feature structures are disabled on all planets regardless of this XML value - - If the global config is `true`, this tag can still disable structures for an individual planet -- Structure generation also requires the planet to be habitable/breathable -#### `` -Enable/disable cave generation. +A procedural galaxy with two archetypes and one type: ```xml -true + + + + + + + + + + + + + + + ``` -Accepted values: -- `true` -- `false` +--- -#### `` -Crater frequency multiplier. +## 11. Pitfalls -```xml -1.5 -``` - -Behavior: - -- `1.0` = default -- `2.0` = double -- `0.5` = half -- Values are clamped to `0.01` - `10.0` - -#### `` -Volcano frequency multiplier. - -```xml -0.5 -``` - -Behavior: - -- `1.0` = default -- `2.0` = double -- `0.5` = half -- Values are clamped to `0.01` - `10.0` - -#### `` -Geode frequency multiplier. - -```xml -2.0 -``` - -Behavior: - -- `1.0` = default -- `2.0` = double -- `0.5` = half -- Values are clamped to `0.01` - `10.0` - -### 6.6 Blocks, ores, and loot - -#### `` -Per-planet custom ore generation. - -Example: - -```xml - - - - -``` - -Important: -- The loader reads ore data from `` attributes -- Do not use nested child tags inside `` -- Per-planet `` overrides the fallback ore mapping from `oreConfig.xml` - -Behavior: -- A non-empty per-planet `` gives that planet custom AR ore properties - - `oreConfig.xml` is only used if the planet does not define its own `` -- If a planet has ore properties from either per-planet `` or matching `oreConfig.xml`, AR denies these `OreGenEvent.GenerateMinable` types on that planet: - - `COAL` - `DIAMOND` - `EMERALD` - `GOLD` - `IRON` - `LAPIS` - `QUARTZ` - `REDSTONE` - `CUSTOM` -- Because AR’s own config-driven ore generator (`Copper`, `Tin`, `Rutile`, `Aluminum`, `Iridium`, `Dilithium`) uses `CUSTOM`, those ores are also suppressed on such planets -- In practice, this means per-planet ore properties replace AR’s normal config ore generation on that planet rather than adding to it -- An empty `` does not count; at least one valid `` entry is required for this behavior -- Mods that generate ores through other paths may still bypass this - -Precedence: -- Per-planet `` in `planetDefs.xml` has highest priority -- If `` is absent on that planet, AR falls back to matching entries from `oreConfig.xml` -- If either of those supplies ore properties for the planet, AR’s normal config-driven ore generation is suppressed on that planet -- If neither per-planet `` nor `oreConfig.xml` provides ore properties, AR falls back to its normal global config-driven ore generation -- `` also has a way of disabling normal oregen - - -##### `block` -Block registry name. Required. - -```xml -block="minecraft:iron_ore" -``` - -##### `meta` -Block metadata. Optional. - -```xml -meta="0" -``` - -##### `minHeight` -Minimum generation height. Required. - -```xml -minHeight="1" -``` - -##### `maxHeight` -Maximum generation height. Required. - -```xml -maxHeight="64" -``` - -##### `clumpSize` -Vein size. Required. - -```xml -clumpSize="8" -``` - -##### `chancePerChunk` -Attempts per chunk. Required. - -```xml -chancePerChunk="20" -``` - -Notes: -- Invalid ore entries are skipped with warnings -- `block` must resolve through `Block.getBlockFromName(...)` - -#### `` -Base terrain block override. - -Accepted formats: -- `modid:block` -- `modid:block:meta` - -Examples: - -```xml -minecraft:stone -or -minecraft:stone:3 -``` - -Notes: -- Only one filler block is stored; if multiple are present, the last valid one wins -- If omitted, terrain defaults to `minecraft:stone` -- If set, the planet’s solid terrain mass uses this block instead of stone -- Natural `minecraft:stone` variants preserve more normal biome-style behavior -- Non-stone filler blocks can suppress normal biome/ore generation -- `` does not disable AR custom ore generation from `` - -#### `` -Laser drill ore list. - -Accepted entry formats: -- OreDictionary name, optionally with count -- item registry name, optionally with count and damage - -Examples: - -```xml -oreIron;3,oreGold;1 -or -minecraft:diamond;1;0,minecraft:redstone;8;0 -``` - -Rules: -- Entries are comma-separated -- Each entry uses semicolon-separated parts - -For OreDictionary entries: -- `oreName` -- `oreName;count` - -For item entries: -- `modid:item` -- `modid:item;count` -- `modid:item;count;damage` - -Notes: -- Invalid ore names or item ids are ignored with warnings -- The raw string is preserved internally as `laserDrillOresRaw` -- This is not tested vs JEI-integration - -#### `` -Geode ore whitelist. - -```xml -oreDiamond,oreEmerald -``` - -Notes: -- Comma-separated -- Entries must exist in OreDictionary -- Invalid names are filtered out - -#### `` -Crater ore whitelist. - -```xml -oreIron,oreGold -``` - -Notes: -- Comma-separated -- Entries must exist in OreDictionary -- Invalid names are filtered out - -#### `` -Ocean block override. (sea block) - -```xml -minecraft:water -``` - -Notes: -- Value is a block resource location -- No metadata is supported here in the XML loader - - -This setting is a full terrain base-material override, not a decorative or secondary filler -#### `` -Required artifact entry. - -Accepted format: -- `item_or_block meta count` - -Examples: - -```xml -minecraft:diamond 0 1 -minecraft:stone 3 16 -``` - -Notes: -- The first token is resolved first as block, then as item -- `meta` defaults to `0` -- `count` defaults to `1` - -### 7.7 Spawn entries - -#### `` -Custom spawn entry. - -Example: - -```xml -minecraft:zombie -``` - -Loader behavior: - -- element text content: - - entity registry name, e.g. `minecraft:zombie` -- supported attributes: - - `weight` - - `groupMin` - - `nbt` - -##### `weight` -Spawn weight. - -```xml -weight="100" -``` - -##### `groupMin` -Minimum group size. - -```xml -groupMin="1" -``` - -##### `nbt` -NBT string passed to the spawn entry. - -```xml -nbt="{CustomName:\"Bob\"}" -``` - -Important parser note: -- The current loader has a bug: - - it reads `groupMin` correctly - - but it also mistakenly reads `groupMax` from the `groupMin` attribute -- As a result, `groupMax` is not actually loaded correctly by the current parser -- For current-code documentation purposes, `groupMax` should not be treated as a reliable working XML input - -Notes: -- If `groupMax` ends up below `groupMin`, it is corrected upward -- Entity lookup first tries registry name, then tries class name -- Invalid NBT can produce fatal configuration errors - -### 7.8 Discovery and progression - -#### `` -Marks the planet as initially known. - -```xml -true -``` - -Accepted values: -- `true` -- `false` - -Notes: -- If true, the planet ID is added to `ARConfiguration.getCurrentConfig().initiallyKnownPlanets` - -### 7.9 Custom weather - -These are used by `WorldProviderPlanet.updateWeather()` when the planet is using custom world info. - -#### `` -Base interval for starting rain. - -```xml -168000 -``` - -#### `` -Base interval for starting thunder. - -```xml -168000 -``` - -#### `` -Extension interval while rain is active. - -```xml -12000 -``` - -#### `` -Extension interval while thunder is active. - -```xml -12000 -``` - -#### `` -Rain mode control. - -```xml -0 -``` - -Meaningful values: -- `-1` = never rain -- `0` = normal cycle -- `1` = always rain - -#### `` -Thunder mode control. - -```xml -0 -``` - -Meaningful values: -- `-1` = never thunder -- `0` = normal cycle -- `1` = always thunder - -Important notes for all weather fields: -- The XML loader uses direct integer parsing here -- Use valid integers -- At runtime, world weather code treats non-positive intervals defensively, but the XML parser itself is not forgiving of malformed values - ---- - -## 7. Value Formats - -### 7.1 Color formats - -Supported by: -- `` -- `` -- `` - -Accepted forms: - -#### RGB floats -```xml -0.5,1,1 -``` - -#### Hex with `0x` -```xml -0x87FFFF -``` - -Notes: -- RGB float input is expected as three comma-separated components -- Hex is parsed after removing `0x` - -### 8.2 Boolean values - -Use: - -```xml -true -false -``` - -Tags using boolean-style values include: -- `` -- `` -- `` -- `` -- `` -- `` -- `` -- `` -- `` -- all `generate...` tags - -### 7.3 Resource-location-like values - -Examples: -- blocks: `minecraft:stone` -- items: `minecraft:diamond` -- biomes: `minecraft:plains` -- entities: `minecraft:zombie` - -Fluids for `` use fluid registry names, such as: -- `hydrogen` -- `oxygen` - -### 7.4 Numeric conventions - -- `100` atmosphere density = Earthlike atmosphere scale -- `100` gravitational multiplier = Earthlike gravity scale -- angles are provided in degrees in XML -- rotational period uses ticks -- sea level uses block Y coordinates - ---- - -## 8. Special Syntax Reference - -### 8.1 `biomeIds` syntax - -Allowed forms: -- `0` -- `minecraft:plains` -- `minecraft:plains;30` - -Combined example: - -```xml -minecraft:plains;30,minecraft:forest;20,12 -``` - -### 8.2 `craterBiomeWeights` syntax - -Allowed form: -- `biome;frequency` - -Example: - -```xml -minecraft:desert;100,minecraft:mesa;60 -``` - -### 8.3 `artifact` syntax - -Format: - -`item_or_block meta count` - -Example: - -```xml -minecraft:diamond 0 1 -``` - -Defaults: -- meta: `0` -- count: `1` - -### 8.4 `fillerBlock` syntax - -Accepted forms: - -```xml -minecraft:stone -or -minecraft:stone:3 -``` - -### 8.5 `spawnable` syntax - -Current reliable format: - -```xml -minecraft:zombie -``` - -With NBT: - -```xml -minecraft:skeleton -``` - -Current parser caveat: -- `groupMax` is not reliably read due to a loader bug - -### 8.6 `oreGen` syntax - -Use attribute-based `` entries: - -```xml - - - -``` - -Do not rely on nested child tags inside `` for loading behavior. - - - -## 9. Practical Examples - -### 9.1 Basic terrestrial planet - -```xml - - 0.7,0.8,1 - 0.4,0.6,1 - 100 - true - 100 - 100 - 0 - 24000 - -``` - -### 9.2 Planet with a moon - -```xml - - 100 - 100 - 100 - 0 - 24000 - - - 0 - false - 16 - 150 - 180 - 24000 - - -``` - -### 9.3 Gas giant with harvestable gases - -```xml - - true - 180 - 220 - 90 - 18000 - hydrogen - -``` - -### 9.4 Binary star system - -```xml - - - - 100 - 100 - 100 - 0 - 24000 - - -``` - -### 9.5 External dimension mapping - -```xml - - 100 - 100 - 140 - 45 - 24000 - -``` - -### 9.6 Planet with custom icon - -```xml - - 120 - 95 - 110 - 270 - 22000 - -``` - -### 9.7 Planet with custom ore generation - -```xml - - 30 - 90 - 80 - 120 - 24000 - - - - - - -``` - -### 9.8 Planet with custom weather - -```xml - - 130 - 100 - 95 - 60 - 24000 - - 6000 - 12000 - 9000 - 6000 - 0 - 0 - -``` - -### 9.9 Planet with custom spawn entries - -```xml - - 80 - 100 - 130 - 180 - 24000 - - minecraft:zombie - minecraft:skeleton - -``` - ---- - -## 10. Common Pitfalls - -### 10.1 `numPlanets`, not `numPlanet` -Attribute name is: - -```xml -numPlanets="..." -``` - -### 10.2 `groupMax` is currently not reliable -Current parser bug: -- `groupMax` is not read correctly -- `groupMin` is mistakenly used for both min and max group size - - -### 10.3 Some author-facing fields from old exports are not real XML inputs -Do not treat exported values such as `avgTemperature` as reliable author-controlled XML settings unless separately confirmed in code. - ---- - -## 11. Fields Intentionally Not Documented Here - -This document intentionally excludes fields that were not confirmed as meaningful current XML inputs. - -Examples: -- fields only written by export code -- fields not meaningfully loaded back -- fields whose behavior was not confirmed when writing this document - ---- - -## 12. Full Example - -```xml - - - - 0.7,0.8,1 - 0.4,0.6,1 - 100 - true - 100 - 100 - 0 - 0 - 24000 - 63 - minecraft:plains;30,minecraft:forest;20 - true - true - true - true - - - 0.9,0.9,0.9 - 0.1,0.1,0.1 - 0 - false - 16 - 150 - 180 - 24000 - true - - - - - true - 180 - 220 - 90 - 18000 - hydrogen - oxygen - true - 70 - 0.6,0.5,0.7 - - - -``` +- **`numPlanets`, not `numPlanet`.** An unrecognised attribute is ignored silently, and the star then + generates nothing — with a warning about a missing entry rather than about a misspelling. +- **Editing the live copy.** It is rewritten on the next save. Edit the template and use + `resetPlanetsFromXML`. +- **`avgTemperature` looks authorable and is not.** It is written by the exporter and recomputed on + load. The same goes for anything else that appears in an exported file but is absent from §7 here: + if the reader has no branch for it, writing it does nothing. +- **Two planets with the same `DIMID`.** Not detected; the second silently replaces the first. +- **A weight of `0`** in `biomeIds` warns and reverts to the default, because a zero-weight entry + would silently never be drawn. --- -## 13. Resources -App to help build universe. https://github.com/DaIsimsiz/planetDefs-Builder/releases +## 12. External tools -) +A community editor for building a catalogue visually: +. It predates the fields introduced by the +3.0.0 line — `mass`, `radius`, `metallicity`, `terrainSource`, `` and `` — so +check its output against §7 before shipping it. From 97101e0fa8f42850e9d74bf850ae3b4483106ea4 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Fri, 14 Aug 2026 16:11:34 +0300 Subject: [PATCH 14/42] feat: a procedural system can hold more than one star - about half of systems draw companions; ids come from a reserved slot - a companion is a body of its system, seated from its own elements - a world is dropped where a companion would tear it away - separations depend on nothing but their own draw, so the zone stays computable --- .../universe/ClusteredGalaxyGenerator.java | 180 +++++++++++++++++- .../unit/ClusteredGalaxyGeneratorTest.java | 12 +- .../test/unit/SystemRetinueTest.java | 163 +++++++++++++++- 3 files changed, 344 insertions(+), 11 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 018608d76..11dec3513 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -32,9 +32,11 @@ * at a hash-chosen cell within the super-cell. * * - *

A procedural system is a bare star (type/size sampled by weight from the seed) with a synthetic - * negative id — it is never in the catalogue and never a dimension, so the id cannot collide with a real - * star-id ({@code 0..N}) or a dim id. Planet CONTENT is a separate concern; this generator places stars only.

+ *

A procedural system is one or more stars (type and size sampled by weight from the seed) with + * synthetic negative ids — never in the catalogue and never a dimension, so an id cannot collide + * with a real star id ({@code 0..N}) or a dim id. About half of systems hold a companion, and a + * companion is a star in its own right: it has its own id, its own orbit about the primary, and its own + * cell, so a world can be bound to it and every world here is lit by all of them.

*/ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { @@ -48,9 +50,50 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { private static final long SALT_TYPE = 0x6L; private static final long SALT_SIZE = 0x7L; private static final long SALT_ID = 0x8L; + private static final long SALT_MULTIPLICITY = 0x9L; + private static final long SALT_COMPANION_COUNT = 0xAL; + private static final long SALT_COMPANION_TYPE = 0xBL; + private static final long SALT_COMPANION_SIZE = 0xCL; + private static final long SALT_COMPANION_SEP = 0xDL; + private static final long SALT_COMPANION_ANG = 0xEL; private static final long SYNTHETIC_ID_RANGE = 2_000_000_000L; // ids in [-2_000_000_000, -1] + // ─── Multiplicity ────────────────────────────────────────────────────────── + // Roughly half of real stars are not alone, and a system that can only ever be one star is a + // model that cannot express the commonest thing in the sky. Every number here is a balance knob; + // what is NOT a knob is that multiplicity belongs inside ONE system — a near-pair of lattice + // seats would be two unrelated systems with two names, two frames and no gravitational relation. + + /** Fraction of systems that hold more than one star. */ + private static final double MULTIPLE_FRACTION = 0.45d; + /** How many companions a multiple system holds, by falling probability: 1, then 2, then 3. */ + private static final double[] COMPANION_COUNT_WEIGHTS = {0.75d, 0.20d, 0.05d}; + /** + * Id slots reserved per system, so a primary and its companions can never collide with each + * other however the hash falls. A system's stars take consecutive ids inside its own slot. + */ + private static final int ID_SLOTS_PER_SYSTEM = 1 + COMPANION_COUNT_WEIGHTS.length; + + /** + * Separation band for a companion, in orbital-distance units — 0.01 AU to 2 000 AU, drawn + * log-uniformly, which is roughly how real separations are distributed over that range. + * + *

The floor is one cell's worth of orbit, so a companion always gets a cell of its own to be + * addressed by. The ceiling is a quarter of the guaranteed clear space around a system, which is + * what lets that clear space state "no two unrelated stars come this close" without a binary ever + * being mistaken for one.

+ */ + private static final int COMPANION_MIN_SEPARATION = 1; + private static final int COMPANION_MAX_SEPARATION = 200_000; + /** + * A retinue cannot survive inside a companion's orbit, nor a companion inside the retinue's: a + * body between roughly a third of the separation and three times it is on an unstable orbit. So a + * separation drawn into the planets' band is pushed to whichever side of it is nearer, and the + * system comes out either circumbinary or widely separated — never impossible. + */ + private static final double STABILITY_FACTOR = 3d; + // Procedural in-system content (bodiesFor). All tunable. Per amendment A#1a each body gets its OWN cell // at a sector offset from the anchor (snapped to that cell's centre); the neighbourhood radius is bounded // by the super-cell partition (minSpacing/2 - margin) so two systems' neighbourhoods never interleave. @@ -213,6 +256,27 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { Set taken = new HashSet<>(); taken.add(cell.cellKey()); + // Every star of the system is a body in it. A companion that existed only on the StellarBody + // would light the worlds here and appear in no sky, on no chart and at no address — which is + // the shape of "expressible in storage, meaningless everywhere else" this whole seam removes. + // It is seated from ITS OWN elements, never from a fresh draw: the star object and the body + // that stands for it have to be the same statement, or one system holds a companion in two + // places at once. + for (StellarBody companion : star.getSubStars()) { + double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY + * AstronomicalBodyHelper.getOrbitalPeriod(companion.getOrbitalDistance(), + star.getMass()); + Seat seat = claimSeat(cell, s, taken, companion.getOrbitalDistance(), + companion.getBaseTheta(), 0d, periodTicks); + if (seat == null) { + continue; + } + bodies.add(new SystemBody(seat.cell, + CellFrame.of(AbsolutePos.ofCellName(cell.cellCentre()), seat.law), + BodyEphemeris.STATIC, SystemBodyKind.STAR, Constants.INVALID_PLANET, + companion.getId(), companion.getOrbitalDistance())); + } + int count = retinueSize(seed, cell); int outermostOrbit = 0; int innermostGiantOrbit = 0; @@ -230,6 +294,9 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { if (orbit > outerBound) { continue; // outside this system's clear space — a bound of the layout, not a failure } + if (!orbitIsStableAmong(star.getSubStars(), orbit)) { + continue; // too near one of this system's other stars for any orbit to survive + } Seat seat = seatBody(seed, cell, i, orbit, star, s, taken); if (seat == null) { continue; // this system's neighbourhood is full — a bound of the layout, not a failure @@ -323,7 +390,12 @@ private static Seat seatBody(long seed, GalacticCoord anchor, int index, int orb double phiDegrees = Math.toDegrees(Math.asin(sinPhi)); double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY * AstronomicalBodyHelper.getOrbitalPeriod(orbit, star.getMass()); + return claimSeat(anchor, s, taken, orbit, baseAngle, phiDegrees, periodTicks); + } + /** Walk the ring from {@code baseAngle} until a free cell turns up, or give up. */ + private static Seat claimSeat(GalacticCoord anchor, long s, Set taken, int orbit, + double baseAngle, double phiDegrees, double periodTicks) { for (int attempt = 0; attempt < NUDGE_ATTEMPTS; attempt++) { BodyEphemeris law = BodyEphemeris.orbit(orbit, baseAngle + attempt * NUDGE_ANGLE, phiDegrees, false, periodTicks, AstronomicalBodyHelper.BLOCKS_PER_ORBIT_UNIT); @@ -495,11 +567,102 @@ private StarSystem fabricate(long seed, long supX, long supY, long supZ) { StellarBody star = new StellarBody(); star.setTemperature(type.temperature); star.setSize((float) (type.minSize + sizeFrac * (type.maxSize - type.minSize))); - star.setId(syntheticId(seed, supX, supY, supZ)); + int primaryId = syntheticId(seed, supX, supY, supZ); + star.setId(primaryId); star.setName("PGS-" + supX + "." + supY + "." + supZ); // procedurally-generated system + addCompanions(seed, supX, supY, supZ, star, primaryId); return new StarSystem(star); } + /** + * Give this system the stars it has beyond the first. + * + *

The generator had never produced one: its own javadoc said "a procedural system is a bare + * star", so every procedural system in the galaxy was single while about half of real stars are + * not. The type layer could always express a hierarchy; what was missing was anything that drew + * one, and an id space in which a companion could be addressed at all.

+ * + *

Ids come from the system's own reserved slot, so a primary and its companions cannot collide + * with each other whatever the hash does. A companion is never larger than its primary — the + * primary is by definition the star its system is named for.

+ */ + private void addCompanions(long seed, long supX, long supY, long supZ, StellarBody primary, + int primaryId) { + if (CellHash.norm(CellHash.of(seed, supX, supY, supZ, SALT_MULTIPLICITY)) >= MULTIPLE_FRACTION) { + return; + } + int count = drawCompanionCount(CellHash.norm( + CellHash.of(seed, supX, supY, supZ, SALT_COMPANION_COUNT))); + GalacticCoord key = cellOf(supX, supY, supZ); + for (int i = 1; i <= count; i++) { + GalaxyGenConfig.StarType type = pickType( + CellHash.ofBody(seed, key, i, SALT_COMPANION_TYPE)); + double sizeFrac = CellHash.norm(CellHash.ofBody(seed, key, i, SALT_COMPANION_SIZE)); + float size = (float) (type.minSize + sizeFrac * (type.maxSize - type.minSize)); + + StellarBody companion = new StellarBody(); + companion.setTemperature(type.temperature); + companion.setSize(Math.min(size, primary.getSize())); + companion.setId(primaryId - i); // the system's own reserved slot; see ID_SLOTS_PER_SYSTEM + companion.setName(primary.getName() + "-" + (char) ('B' + i - 1)); + companion.setOrbitalDistance(drawSeparation( + CellHash.norm(CellHash.ofBody(seed, key, i, SALT_COMPANION_SEP)))); + companion.setBaseTheta( + CellHash.norm(CellHash.ofBody(seed, key, i, SALT_COMPANION_ANG)) * 2d * Math.PI); + primary.addSubStar(companion); + } + } + + /** How many companions, from a falling distribution over {@link #COMPANION_COUNT_WEIGHTS}. */ + private static int drawCompanionCount(double u) { + double acc = 0d; + for (int i = 0; i < COMPANION_COUNT_WEIGHTS.length; i++) { + acc += COMPANION_COUNT_WEIGHTS[i]; + if (u < acc) { + return i + 1; + } + } + return COMPANION_COUNT_WEIGHTS.length; + } + + /** + * A companion's separation: log-uniform across the band, bounded by the room the system has. + * + *

It depends on NOTHING but its own draw. That is deliberate and it is what makes the system + * buildable at all: a star's zone is a function of the system's luminosity, and the luminosity is + * a function of where its stars stand, so a separation chosen to avoid the zone would be chosen + * against a zone that its own choice then moved. Measured 2026-08-14: one such pass left a + * companion at 177 AU inside planets running out to 180 AU, because pushing the other two + * companions inward had brightened the system fivefold and widened the very band being avoided. + * The dependency runs one way instead — stars first, and the retinue accommodates them.

+ */ + private static int drawSeparation(double u) { + double separation = COMPANION_MIN_SEPARATION + * Math.pow((double) COMPANION_MAX_SEPARATION / COMPANION_MIN_SEPARATION, u); + return (int) Math.max(COMPANION_MIN_SEPARATION, + Math.min(UniverseScale.MAX_NAMED_ORBIT_UNITS, Math.round(separation))); + } + + /** + * Whether a planet at {@code orbit} could survive among these stars: not between roughly a third + * of a companion's separation and three times it, where neither a circumbinary nor a satellite + * orbit is stable. + */ + private static boolean orbitIsStableAmong(Iterable companions, int orbit) { + for (StellarBody companion : companions) { + double separation = companion.getOrbitalDistance(); + if (orbit > separation / STABILITY_FACTOR && orbit < separation * STABILITY_FACTOR) { + return false; + } + } + return true; + } + + /** The super-cell index triple as a coordinate, for the per-index hash draws. */ + private static GalacticCoord cellOf(long supX, long supY, long supZ) { + return GalacticCoord.ofSectorLocal(supX, supY, supZ, 0L, 0L, 0L); + } + private GalaxyGenConfig.StarType pickType(long h) { long r = Math.floorMod(h, totalStarWeight); // long arithmetic — overflow-safe over the weight sum GalaxyGenConfig.StarType last = null; @@ -513,8 +676,15 @@ private GalaxyGenConfig.StarType pickType(long h) { return last; // config.starTypes is never empty } + /** + * The primary's synthetic id: negative, so it can never collide with a catalogued star id + * ({@code 0..N}) or a dim id, and spaced {@link #ID_SLOTS_PER_SYSTEM} apart so a system's + * companions have ids of their own below it that belong to no other system. + */ private static int syntheticId(long seed, long supX, long supY, long supZ) { - return -(1 + (int) Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_ID), SYNTHETIC_ID_RANGE)); + long slot = Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_ID), + SYNTHETIC_ID_RANGE / ID_SLOTS_PER_SYSTEM); + return -(1 + (int) (slot * ID_SLOTS_PER_SYSTEM)); } private static final class Generated { diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index b47d9d4a2..063bdf5f4 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -382,9 +382,19 @@ public void proceduralBodiesGetTheirOwnCellsInsideTheSuperCell() { assertTrue(a.get(0).name().sameCell(anchor)); assertEquals(0, a.get(0).name().localX()); + // Every body names a star OF THIS SYSTEM — the primary, or one of its companions, which + // are stars in their own right with ids of their own. + Set systemStars = new HashSet<>(); + systemStars.add(a.get(0).starId()); + for (zmaster587.advancedRocketry.api.dimension.solar.StellarBody companion + : gen.systemAt(SEED, anchor).get().star().getSubStars()) { + systemStars.add(companion.getId()); + } + boolean sawOwnCell = false; for (SystemBody body : a) { - assertEquals("every body belongs to the system's star", a.get(0).starId(), body.starId()); + assertTrue("body names star " + body.starId() + ", which is not one of this system's", + systemStars.contains(body.starId())); assertFalse("procedural bodies are not descend targets yet", body.isDescendTarget()); // Snapped to its own cell's centre. assertEquals(0, body.name().localX()); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java index cda845d35..b489ae080 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java @@ -22,6 +22,7 @@ import zmaster587.advancedRocketry.universe.SystemBodyKind; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -135,14 +136,20 @@ public void theInvariantHoldsEvenWhenTheNeighbourhoodIsCrampedForRoom() { // ─── what a system loses when it does not fit ────────────────────────────── @Test - public void atTheShippedScaleNoSystemLosesABodyAtAll() { - // The bound is a GUARD, not a mechanic anybody meets. Measured 2026-08-14: the widest zone - // any shipped star archetype can draw is 569 AU against a clear space of 5 000 — a factor of - // nearly nine. If this ever goes red, either the star table gained something far hotter or - // the spacing was cut by two orders, and both are worth knowing about deliberately. + public void atTheShippedScaleASingleStarLosesNoBodyAtAll() { + // The clear-space bound is a GUARD, not a mechanic anybody meets. Measured 2026-08-14: the + // widest zone any shipped star archetype can draw is 569 AU against a clear space of 5 000 — + // a factor of nearly nine. If this ever goes red, either the star table gained something far + // hotter or the spacing was cut by two orders, and both are worth knowing about deliberately. + // + // SINGLE stars only: a system with a companion loses worlds to the band around it, which is + // a different mechanism with its own test and must not be able to mask this one. ClusteredGalaxyGenerator g = gen(SPACING); int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { + if (!g.systemAt(SEED, anchor).get().star().getSubStars().isEmpty()) { + continue; + } int wanted = ClusteredGalaxyGenerator.retinueSize(SEED, anchor); int got = 0; for (SystemBody b : g.bodiesFor(SEED, anchor)) { @@ -192,6 +199,152 @@ public void aCrampedSystemDropsBodiesAndNeverMovesTheOnesItKeeps() { droppedSomewhere > 0); } + @Test + public void aCompanionCostsItsSystemTheWorldsItStandsAmong() { + // The other half of the same rule: where a star sits, worlds cannot. A multiple system is + // therefore allowed to hold fewer worlds than its retinue drew — and the test exists so that + // "fewer" stays a consequence of the companion rather than of something silently going wrong. + ClusteredGalaxyGenerator g = gen(SPACING); + int multiple = 0; + int lostSome = 0; + for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { + if (g.systemAt(SEED, anchor).get().star().getSubStars().isEmpty()) { + continue; + } + multiple++; + int have = 0; + for (SystemBody b : g.bodiesFor(SEED, anchor)) { + if (b.kind() == SystemBodyKind.PLANET || b.kind() == SystemBodyKind.GAS_GIANT) { + have++; + } + } + if (have < ClusteredGalaxyGenerator.retinueSize(SEED, anchor)) { + lostSome++; + } + } + assertTrue("the sweep must find multiple systems", multiple > 10); + assertTrue("a companion must cost its system something, or the band is not being applied", + lostSome > 0); + assertTrue("but it must not cost every system everything, saw " + lostSome + "/" + multiple, + lostSome < multiple); + } + + // ─── multiplicity ────────────────────────────────────────────────────────── + + @Test + public void someSystemsHoldMoreThanOneStarAndMostDoNot() { + // The generator had never produced a companion — its own javadoc said so — while about half + // of real stars are not alone. What is pinned is the SHAPE: multiple systems are common but + // not the rule, and a system never holds an unbounded pile of stars. + ClusteredGalaxyGenerator g = gen(SPACING); + int systems = 0; + int multiple = 0; + int mostStars = 0; + for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { + StellarBody star = g.systemAt(SEED, anchor).get().star(); + int stars = 1 + star.getSubStars().size(); + systems++; + if (stars > 1) { + multiple++; + } + mostStars = Math.max(mostStars, stars); + } + assertTrue("the sweep must find systems", systems > 20); + assertTrue("multiple systems must exist at all", multiple > 0); + assertTrue("and single ones must stay the majority, saw " + multiple + "/" + systems, + multiple * 2 < systems * 3); + assertTrue("a system must be able to hold three stars, saw at most " + mostStars, + mostStars >= 2); + } + + @Test + public void everyStarOfASystemHasAnIdOfItsOwn() { + // The defect that made a companion unaddressable: it was handed the primary's id, so no + // starId value could ever mean "I orbit the companion" and a companion could own no world. + ClusteredGalaxyGenerator g = gen(SPACING); + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { + StellarBody star = g.systemAt(SEED, anchor).get().star(); + Set ids = new HashSet<>(); + assertTrue(ids.add(star.getId())); + for (StellarBody companion : star.getSubStars()) { + assertTrue("companion " + companion.getName() + " repeats an id of its own system", + ids.add(companion.getId())); + assertTrue("a procedural star id must stay synthetic (negative)", + companion.getId() < 0); + assertTrue("a companion is never larger than the star its system is named for", + companion.getSize() <= star.getSize()); + } + checked++; + } + assertTrue(checked > 20); + } + + @Test + public void aCompanionIsABodyOfItsSystemStandingAtItsOwnSeparation() { + // A companion that existed only on the star object would light the worlds here and appear at + // no address at all. It must be a body, in a cell of its own, exactly where its own elements + // put it — the star object and the body standing for it are one statement. + ClusteredGalaxyGenerator g = gen(SPACING); + int checkedCompanions = 0; + for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { + StellarBody star = g.systemAt(SEED, anchor).get().star(); + if (star.getSubStars().isEmpty()) { + continue; + } + Map starBodies = new HashMap<>(); + for (SystemBody b : g.bodiesFor(SEED, anchor)) { + if (b.kind() == SystemBodyKind.STAR) { + starBodies.put(b.starId(), b); + } + } + for (StellarBody companion : star.getSubStars()) { + SystemBody body = starBodies.get(companion.getId()); + assertNotNull("companion " + companion.getName() + " is in no body list", body); + assertFalse("a companion must hold a cell of its own, not the primary's", + body.name().sameCell(anchor)); + double placed = body.absoluteAt(0L).distanceTo( + zmaster587.advancedRocketry.space.AbsolutePos.ofCellName(anchor)); + double expected = (double) companion.getOrbitalDistance() + * zmaster587.advancedRocketry.util.AstronomicalBodyHelper.BLOCKS_PER_ORBIT_UNIT; + assertEquals("a companion stands at the separation its own elements state", + expected, placed, expected * 1e-6d + 2d); + checkedCompanions++; + } + } + assertTrue("the sweep must contain companions", checkedCompanions > 3); + } + + @Test + public void noWorldSitsWhereAnotherStarWouldTearItAway() { + // A planet between roughly a third of a companion's separation and three times it is on an + // orbit neither a circumbinary nor a satellite path can hold. The retinue accommodates the + // stars rather than the stars accommodating the retinue — which is also the only order that + // can be computed, because a star's zone follows the system's luminosity and the luminosity + // follows where its stars stand. + ClusteredGalaxyGenerator g = gen(SPACING); + int checked = 0; + for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { + StellarBody star = g.systemAt(SEED, anchor).get().star(); + if (star.getSubStars().isEmpty()) { + continue; + } + for (SystemBody b : g.bodiesFor(SEED, anchor)) { + if (b.kind() != SystemBodyKind.PLANET && b.kind() != SystemBodyKind.GAS_GIANT) { + continue; + } + for (StellarBody companion : star.getSubStars()) { + double sep = companion.getOrbitalDistance(); + assertTrue("a world at " + b.orbitalDistance() + " sits beside a star at " + sep + + ", where no orbit survives", + b.orbitalDistance() <= sep / 3d || b.orbitalDistance() >= sep * 3d); + checked++; + } + } + } + assertTrue("the sweep must contain multiple systems with worlds", checked > 10); + } + // ─── E1: a long-tailed body count ────────────────────────────────────────── @Test From c3451c174970673add10dbb401a63e2ee6cb77e7 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Fri, 14 Aug 2026 16:21:16 +0300 Subject: [PATCH 15/42] fix: a coordinate can no longer name a position it cannot express - AbsolutePos holds a sector triple and an in-cell offset, not three block longs - GalacticCoord.absoluteX/Y/Z retired: the product overflowed seven orders before the sector index did, silently - a frame's base is saved as sector + offset - distances come from the two deltas; a block vector saturates instead of wrapping --- .../command/test/TestProbeCommand.java | 23 ++- .../advancedRocketry/space/AbsolutePos.java | 155 +++++++++++++----- .../advancedRocketry/space/GalacticCoord.java | 9 +- .../advancedRocketry/universe/CellFrame.java | 15 +- .../test/unit/GalacticCoordTest.java | 68 ++++++-- 5 files changed, 202 insertions(+), 68 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 081732d2b..b549a6cb9 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -3253,10 +3253,22 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] // "bearing", not "dir": the feed below already emits a "dir" per // body, measured from the CELL's observer for the sky, and a reader // matching on the substring could not tell the two apart. + // Taken as a sector delta plus an offset delta, never as the + // difference of two whole-block absolutes: those cannot express the + // coordinates the sector grid can name. .append(",\"bearing\":[") - .append(bodyAt.absoluteX() - e.coord.absoluteX()).append(',') - .append(bodyAt.absoluteY() - e.coord.absoluteY()).append(',') - .append(bodyAt.absoluteZ() - e.coord.absoluteZ()).append(']') + .append(zmaster587.advancedRocketry.space.AbsolutePos + .ofCellName(bodyAt).minus( + zmaster587.advancedRocketry.space.AbsolutePos + .ofCellName(e.coord)).dx()).append(',') + .append(zmaster587.advancedRocketry.space.AbsolutePos + .ofCellName(bodyAt).minus( + zmaster587.advancedRocketry.space.AbsolutePos + .ofCellName(e.coord)).dy()).append(',') + .append(zmaster587.advancedRocketry.space.AbsolutePos + .ofCellName(bodyAt).minus( + zmaster587.advancedRocketry.space.AbsolutePos + .ofCellName(e.coord)).dz()).append(']') .append(",\"distance\":") .append((long) Math.sqrt(e.coord.staticFrameDistanceSqTo(bodyAt))) // "distance" is to the body's CENTRE — what the descent trigger @@ -4543,8 +4555,9 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] zmaster587.advancedRocketry.space.AbsolutePos origin = zmaster587.advancedRocketry.space.SpaceSubsystem.cellFrameOriginAt(name, clock); send(sender, "{\"ok\":true,\"cellKey\":\"" + name.cellKey() + "\",\"clock\":" + clock - + ",\"originX\":" + origin.x() + ",\"originY\":" + origin.y() - + ",\"originZ\":" + origin.z() + "}"); + + ",\"originSector\":[" + origin.sectorX() + "," + origin.sectorY() + "," + + origin.sectorZ() + "],\"originOffset\":[" + origin.localX() + "," + + origin.localY() + "," + origin.localZ() + "]}"); return; } // forget-name : drop the RECORDED cell name of a dimension, so the next query has to diff --git a/src/main/java/zmaster587/advancedRocketry/space/AbsolutePos.java b/src/main/java/zmaster587/advancedRocketry/space/AbsolutePos.java index cf19ebdf3..5a3a0c679 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/AbsolutePos.java +++ b/src/main/java/zmaster587/advancedRocketry/space/AbsolutePos.java @@ -1,7 +1,7 @@ package zmaster587.advancedRocketry.space; /** - * A position in absolute galactic blocks at one stated moment. + * A position in absolute galactic space at one stated moment. * *

This is deliberately NOT a {@link GalacticCoord}. A {@code GalacticCoord} is a cell NAME plus an * offset inside that cell's frame, and a cell's frame moves: its origin is the position of the body @@ -9,80 +9,150 @@ * from "which cell it is in and where inside it", and the two must not share a type — not for * tidiness, but because {@link GalacticCoord#ofSectorLocal} carries an out-of-range offset into * the sector triple. Expressing a frame-displaced position as a {@code GalacticCoord} would therefore - * silently RENAME the cell the moment the frame origin drifts more than half a cell from - * {@code sector * CELL}, which is a routine amount of orbital travel.

+ * silently RENAME the cell the moment the frame origin drifts more than half a cell from the cell's + * own grid position, which is a routine amount of orbital travel.

* *

An absolute position is only ever an intermediate: it exists to be subtracted from another one at * the same tick, giving a {@link BlockDelta} — a direction and a true distance. Nothing is stored * as one and nothing is addressed by one: what goes on disk is always a cell name plus an in-cell * offset, never a value whose meaning depends on the tick it happened to be written at.

* - *

Immutable value type. As with {@link GalacticCoord#absoluteX()}, the {@code long} arithmetic can - * overflow at extreme sector magnitudes; that is the same bound the sectorized coordinate already - * carries and is far outside any generated galaxy.

+ *

Why it is sectorised, and not three block counts

+ * + *

It used to hold three raw block {@code long}s. A sector index reaches 9.2·1018, + * while {@code sector * CELL} overflows a {@code long} at 2.9·1011 — so the + * coordinate system could NAME positions this type could not express, over seven orders of magnitude, + * silently and with no error. Nothing caught it because nothing had yet been placed far enough out. + * Holding a sector triple and an in-cell offset removes the ceiling entirely: the whole addressable + * range is expressible, and a distance is computed from the two deltas rather than from a product + * that cannot fit.

+ * + *

Immutable value type.

*/ public final class AbsolutePos { - /** Absolute (0,0,0) — the centre of the origin cell of a static frame. */ - public static final AbsolutePos ORIGIN = new AbsolutePos(0L, 0L, 0L); + /** Absolute origin: sector {@code (0,0,0)}, offset {@code (0,0,0)}. */ + public static final AbsolutePos ORIGIN = new AbsolutePos(0L, 0L, 0L, 0L, 0L, 0L); - private final long x; - private final long y; - private final long z; + private final long sectorX; + private final long sectorY; + private final long sectorZ; + private final long localX; // canonical: [-HALF_CELL, HALF_CELL) + private final long localY; + private final long localZ; - private AbsolutePos(long x, long y, long z) { - this.x = x; - this.y = y; - this.z = z; + private AbsolutePos(long sectorX, long sectorY, long sectorZ, + long localX, long localY, long localZ) { + this.sectorX = sectorX; + this.sectorY = sectorY; + this.sectorZ = sectorZ; + this.localX = localX; + this.localY = localY; + this.localZ = localZ; } + /** Build from a sector triple and a (possibly out-of-range) offset triple, carrying the overflow. */ + public static AbsolutePos ofSectorLocal(long sectorX, long sectorY, long sectorZ, + long localX, long localY, long localZ) { + long carryX = Math.floorDiv(localX + GalacticCoord.HALF_CELL, GalacticCoord.CELL); + long carryY = Math.floorDiv(localY + GalacticCoord.HALF_CELL, GalacticCoord.CELL); + long carryZ = Math.floorDiv(localZ + GalacticCoord.HALF_CELL, GalacticCoord.CELL); + return new AbsolutePos( + sectorX + carryX, sectorY + carryY, sectorZ + carryZ, + localX - carryX * GalacticCoord.CELL, + localY - carryY * GalacticCoord.CELL, + localZ - carryZ * GalacticCoord.CELL); + } + + /** + * Build from a raw block triple, i.e. an offset from the origin cell. Exact for anything inside + * the range a {@code long} of blocks can hold; beyond that, state the sectors. + */ public static AbsolutePos of(long x, long y, long z) { - return new AbsolutePos(x, y, z); + return ofSectorLocal(0L, 0L, 0L, x, y, z); } /** - * The absolute position a cell NAME denotes under a STATIC frame: {@code sector * CELL}. This is - * the frame origin of a void cell — one with no primary to ride, so it never moves — - * and the fallback for any cell whose primary cannot be resolved. + * The absolute position a cell NAME denotes under a STATIC frame. This is the frame origin of a + * void cell — one with no primary to ride, so it never moves — and the fallback for + * any cell whose primary cannot be resolved. */ public static AbsolutePos ofCellName(GalacticCoord name) { if (name == null) { return ORIGIN; } - return new AbsolutePos(name.sectorX() * GalacticCoord.CELL, - name.sectorY() * GalacticCoord.CELL, - name.sectorZ() * GalacticCoord.CELL); + // The cell's own grid position, and ONLY that: a name denotes a CELL. Where something stands + // inside that cell is carried separately, by the frame's law, and adding it here would count + // the offset twice for every body in the game. + return new AbsolutePos(name.sectorX(), name.sectorY(), name.sectorZ(), 0L, 0L, 0L); } - public long x() { return x; } - public long y() { return y; } - public long z() { return z; } + public long sectorX() { return sectorX; } + public long sectorY() { return sectorY; } + public long sectorZ() { return sectorZ; } + + public long localX() { return localX; } + public long localY() { return localY; } + public long localZ() { return localZ; } /** This position displaced by {@code delta}. */ public AbsolutePos plus(BlockDelta delta) { - return delta == null ? this : new AbsolutePos(x + delta.dx(), y + delta.dy(), z + delta.dz()); + return delta == null ? this : plus(delta.dx(), delta.dy(), delta.dz()); } /** This position displaced by a raw block triple. */ public AbsolutePos plus(long dx, long dy, long dz) { - return new AbsolutePos(x + dx, y + dy, z + dz); + return ofSectorLocal(sectorX, sectorY, sectorZ, localX + dx, localY + dy, localZ + dz); } - /** The vector FROM {@code from} TO this position — the observer→body direction when - * {@code from} is the observer. */ + /** + * The vector FROM {@code from} TO this position — the observer→body direction when + * {@code from} is the observer. + * + *

Saturates instead of wrapping. A separation that does not fit in a {@code long} of blocks is + * one between things in different galaxies, where a block vector is not the useful answer anyway; + * what must never happen is that it comes back as a small number pointing the wrong way.

+ */ public BlockDelta minus(AbsolutePos from) { - return from == null ? BlockDelta.of(x, y, z) - : BlockDelta.of(x - from.x, y - from.y, z - from.z); + if (from == null) { + return BlockDelta.of(saturatingBlocks(sectorX, localX), + saturatingBlocks(sectorY, localY), saturatingBlocks(sectorZ, localZ)); + } + return BlockDelta.of( + saturatingBlocks(sectorX - from.sectorX, localX - from.localX), + saturatingBlocks(sectorY - from.sectorY, localY - from.localY), + saturatingBlocks(sectorZ - from.sectorZ, localZ - from.localZ)); + } + + /** {@code sectors * CELL + local}, held at the {@code long} bounds rather than wrapping past them. */ + private static long saturatingBlocks(long sectors, long local) { + if (sectors > Long.MAX_VALUE / GalacticCoord.CELL) { + return Long.MAX_VALUE; + } + if (sectors < Long.MIN_VALUE / GalacticCoord.CELL) { + return Long.MIN_VALUE; + } + long scaled = sectors * GalacticCoord.CELL; + long sum = scaled + local; + if (((scaled ^ sum) & (local ^ sum)) < 0L) { + return local > 0L ? Long.MAX_VALUE : Long.MIN_VALUE; + } + return sum; } - /** Squared distance to {@code other}, in blocks². Both must be evaluated at the SAME tick. */ + /** + * Squared distance to {@code other}, in blocks². Both must be evaluated at the SAME tick. + * + *

Computed from the sector delta plus the offset delta, so nearby positions stay exact at any + * magnitude and distant ones do not overflow on the way to being measured.

+ */ public double distanceSqTo(AbsolutePos other) { if (other == null) { return 0.0; } - double dx = (double) other.x - x; - double dy = (double) other.y - y; - double dz = (double) other.z - z; + double dx = (double) (other.sectorX - sectorX) * GalacticCoord.CELL + (other.localX - localX); + double dy = (double) (other.sectorY - sectorY) * GalacticCoord.CELL + (other.localY - localY); + double dz = (double) (other.sectorZ - sectorZ) * GalacticCoord.CELL + (other.localZ - localZ); return dx * dx + dy * dy + dz * dz; } @@ -100,19 +170,24 @@ public boolean equals(Object o) { return false; } AbsolutePos other = (AbsolutePos) o; - return x == other.x && y == other.y && z == other.z; + return sectorX == other.sectorX && sectorY == other.sectorY && sectorZ == other.sectorZ + && localX == other.localX && localY == other.localY && localZ == other.localZ; } @Override public int hashCode() { - int result = Long.hashCode(x); - result = 31 * result + Long.hashCode(y); - result = 31 * result + Long.hashCode(z); + int result = Long.hashCode(sectorX); + result = 31 * result + Long.hashCode(sectorY); + result = 31 * result + Long.hashCode(sectorZ); + result = 31 * result + Long.hashCode(localX); + result = 31 * result + Long.hashCode(localY); + result = 31 * result + Long.hashCode(localZ); return result; } @Override public String toString() { - return "AbsolutePos[" + x + "," + y + "," + z + "]"; + return "AbsolutePos[sector=(" + sectorX + "," + sectorY + "," + sectorZ + "), offset=(" + + localX + "," + localY + "," + localZ + ")]"; } } diff --git a/src/main/java/zmaster587/advancedRocketry/space/GalacticCoord.java b/src/main/java/zmaster587/advancedRocketry/space/GalacticCoord.java index a75f0c379..40ccbb46d 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/GalacticCoord.java +++ b/src/main/java/zmaster587/advancedRocketry/space/GalacticCoord.java @@ -88,10 +88,11 @@ public static GalacticCoord ofAbsolute(long absX, long absY, long absZ) { public int localY() { return localY; } public int localZ() { return localZ; } - /** Absolute X in blocks. May overflow {@code long} at extreme sector magnitudes (see class doc). */ - public long absoluteX() { return sectorX * CELL + localX; } - public long absoluteY() { return sectorY * CELL + localY; } - public long absoluteZ() { return sectorZ * CELL + localZ; } + // absoluteX/Y/Z — sector * CELL + local — are gone. A sector index reaches 9.2e18 while the + // product overflows at 2.9e11, so they could NAME a position they could not express, silently, + // over seven orders of magnitude. Nothing materialises a single global block absolute any more: + // a distance comes from the sector delta plus the offset delta (staticFrameDistanceTo below, or + // AbsolutePos for a position at a tick), which is exact nearby and cannot overflow far away. /** {@code true} iff {@code other} is in the same cell (equal sector triple) as this coordinate. */ public boolean sameCell(GalacticCoord other) { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/CellFrame.java b/src/main/java/zmaster587/advancedRocketry/universe/CellFrame.java index 7419b4788..142f44365 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/CellFrame.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/CellFrame.java @@ -62,9 +62,14 @@ public boolean isStatic() { public void writeToNBT(NBTTagCompound nbt) { NBTTagCompound sub = new NBTTagCompound(); - sub.setLong("bx", base.x()); - sub.setLong("by", base.y()); - sub.setLong("bz", base.z()); + // The base is written as a sector triple plus an in-cell offset, for the same reason the type + // holds one: a single block absolute cannot express the coordinates the sector grid can name. + sub.setLong("bsx", base.sectorX()); + sub.setLong("bsy", base.sectorY()); + sub.setLong("bsz", base.sectorZ()); + sub.setLong("blx", base.localX()); + sub.setLong("bly", base.localY()); + sub.setLong("blz", base.localZ()); law.writeToNBT(sub); // nested sub-tag "ephemeris" nbt.setTag("frame", sub); } @@ -79,7 +84,9 @@ public static CellFrame readFromNBT(NBTTagCompound nbt, GalacticCoord name) { return staticAt(name); } NBTTagCompound sub = nbt.getCompoundTag("frame"); - return new CellFrame(AbsolutePos.of(sub.getLong("bx"), sub.getLong("by"), sub.getLong("bz")), + return new CellFrame(AbsolutePos.ofSectorLocal( + sub.getLong("bsx"), sub.getLong("bsy"), sub.getLong("bsz"), + sub.getLong("blx"), sub.getLong("bly"), sub.getLong("blz")), BodyEphemeris.readFromNBT(sub)); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalacticCoordTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalacticCoordTest.java index 8166e028e..5edf27744 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/GalacticCoordTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalacticCoordTest.java @@ -2,6 +2,7 @@ import net.minecraft.nbt.NBTTagCompound; import org.junit.Test; +import zmaster587.advancedRocketry.space.AbsolutePos; import zmaster587.advancedRocketry.space.GalacticCoord; import static org.junit.Assert.assertEquals; @@ -30,15 +31,52 @@ private static void assertLocalCanonical(GalacticCoord c) { assertTrue("localZ in [-HALF, HALF)", c.localZ() >= -HALF && c.localZ() < HALF); } + + // The sector+local identity, read through the type that can hold it. GalacticCoord no longer + // materialises a whole-block absolute of its own: the product overflows a long seven orders + // before the sector index does, so the coordinate could name positions it could not express. + + private static AbsolutePos wholeOf(GalacticCoord c) { + return AbsolutePos.ofSectorLocal(c.sectorX(), c.sectorY(), c.sectorZ(), + c.localX(), c.localY(), c.localZ()); + } + + private static long blocksX(GalacticCoord c) { + return wholeOf(c).minus(AbsolutePos.ORIGIN).dx(); + } + + private static long blocksY(GalacticCoord c) { + return wholeOf(c).minus(AbsolutePos.ORIGIN).dy(); + } + + private static long blocksZ(GalacticCoord c) { + return wholeOf(c).minus(AbsolutePos.ORIGIN).dz(); + } + + @Test + public void aCoordinateBeyondTheOldBlockCeilingStillMeasuresCorrectly() { + // The defect R4 removes: sector * CELL overflows a long at 2.9e11 while a sector index runs + // to 9.2e18. Two coordinates out where the product cannot fit must still be a cell apart — + // under the old arithmetic the difference wrapped and came back small, pointing anywhere. + long farOut = 1_000_000_000_000L; // 3.4 orders past where the product stops fitting + GalacticCoord a = GalacticCoord.ofSectorLocal(farOut, 0L, 0L, 0L, 0L, 0L); + GalacticCoord b = GalacticCoord.ofSectorLocal(farOut + 1L, 0L, 0L, 0L, 0L, 0L); + + assertEquals("one cell apart, however far out they are", + (double) CELL, a.staticFrameDistanceTo(b), 1.0); + assertEquals("and the same measured through an absolute position", (double) CELL, + AbsolutePos.ofCellName(a).distanceTo(AbsolutePos.ofCellName(b)), 1.0); + } + @Test public void absoluteRoundTripWithinCell() { GalacticCoord c = GalacticCoord.ofAbsolute(123L, -456L, 789L); assertEquals(0L, c.sectorX()); assertEquals(0L, c.sectorY()); assertEquals(0L, c.sectorZ()); - assertEquals(123L, c.absoluteX()); - assertEquals(-456L, c.absoluteY()); - assertEquals(789L, c.absoluteZ()); + assertEquals(123L, blocksX(c)); + assertEquals(-456L, blocksY(c)); + assertEquals(789L, blocksZ(c)); assertLocalCanonical(c); } @@ -48,18 +86,18 @@ public void absoluteRoundTripAcrossManyCells() { long ay = -12L * CELL - 5L; long az = 4L * CELL - HALF; // lands exactly on a cell's lower edge GalacticCoord c = GalacticCoord.ofAbsolute(ax, ay, az); - assertEquals(ax, c.absoluteX()); - assertEquals(ay, c.absoluteY()); - assertEquals(az, c.absoluteZ()); + assertEquals(ax, blocksX(c)); + assertEquals(ay, blocksY(c)); + assertEquals(az, blocksZ(c)); assertLocalCanonical(c); } @Test public void sectorLocalIdentityHolds() { GalacticCoord c = GalacticCoord.ofSectorLocal(5L, -3L, 8L, 100L, -200L, 300L); - assertEquals(5L * CELL + 100L, c.absoluteX()); - assertEquals(-3L * CELL - 200L, c.absoluteY()); - assertEquals(8L * CELL + 300L, c.absoluteZ()); + assertEquals(5L * CELL + 100L, blocksX(c)); + assertEquals(-3L * CELL - 200L, blocksY(c)); + assertEquals(8L * CELL + 300L, blocksZ(c)); } @Test @@ -67,9 +105,9 @@ public void localOffsetIsRenormalisedWithSectorCarry() { // Local offsets far outside a cell must fold back in and carry into the sector. GalacticCoord c = GalacticCoord.ofSectorLocal(0L, 0L, 0L, CELL + 10L, -CELL - 10L, 3L * CELL); assertLocalCanonical(c); - assertEquals(CELL + 10L, c.absoluteX()); - assertEquals(-CELL - 10L, c.absoluteY()); - assertEquals(3L * CELL, c.absoluteZ()); + assertEquals(CELL + 10L, blocksX(c)); + assertEquals(-CELL - 10L, blocksY(c)); + assertEquals(3L * CELL, blocksZ(c)); } @Test @@ -119,7 +157,7 @@ public void cellCentreZeroesLocalAndStaysInCell() { assertEquals(0, centre.localY()); assertEquals(0, centre.localZ()); // The centre of sector s sits at absolute s*CELL. - assertEquals(7L * CELL, centre.absoluteX()); + assertEquals(7L * CELL, blocksX(centre)); } @Test @@ -152,7 +190,7 @@ public void integrationDoesNotDriftOverManySteps() { GalacticCoord oneShot = GalacticCoord.ORIGIN.plusLocal(7_000_000L, 0L, 0L); assertEquals(oneShot, stepwise); - assertEquals(7_000_000L, stepwise.absoluteX()); + assertEquals(7_000_000L, blocksX(stepwise)); } @Test @@ -161,7 +199,7 @@ public void plusLocalCarriesAcrossCellBoundary() { GalacticCoord crossed = near.plusLocal(20L, 0L, 0L); assertEquals(1L, crossed.sectorX()); assertLocalCanonical(crossed); - assertEquals(HALF + 10L, crossed.absoluteX()); + assertEquals(HALF + 10L, blocksX(crossed)); } @Test From c5c0b3d424fffc6271b91ce30b8a1846cad3089e Mon Sep 17 00:00:00 2001 From: StannisMod Date: Fri, 14 Aug 2026 17:27:37 +0300 Subject: [PATCH 16/42] feat: a galaxy becomes a place with a centre, an edge and a profile - Galaxies are seated objects; the percolating blob mask is gone - A galaxy's radius is drawn conditional on its type - Systems are placed by the owning galaxy's density profile - Galactic rotation lands as an analytic, tested law - The galaxy index is derived from the sector, never stored - Reserve the home galaxy so authored content is not intergalactic - Sweep find-procedural by super-cell instead of by cell --- docs/README_PLANETDEFS.md | 30 +- .../command/test/TestProbeCommand.java | 55 ++- .../universe/ClusteredGalaxyGenerator.java | 65 +++- .../advancedRocketry/universe/Galaxy.java | 273 +++++++++++++++ .../universe/GalaxyField.java | 242 +++++++++++++ .../universe/GalaxyGenConfig.java | 134 ++++++- .../universe/UniverseRegistry.java | 9 + .../universe/UniverseScale.java | 77 ++++ .../util/AstronomicalBodyHelper.java | 11 + .../util/XMLPlanetLoader.java | 29 +- .../test/integration/SystemContentTest.java | 4 +- .../test/integration/XMLPlanetLoaderTest.java | 16 +- .../ProceduralPlanetRealizationE2ETest.java | 15 +- .../unit/ClusteredGalaxyGeneratorTest.java | 152 ++++---- .../test/unit/GalaxyFieldTest.java | 330 ++++++++++++++++++ .../test/unit/GalaxyTest.java | 202 +++++++++++ .../unit/InterstellarLegDistanceTest.java | 2 +- .../test/unit/PlanetRealizationTest.java | 6 +- .../test/unit/SystemRetinueTest.java | 4 +- .../test/unit/UniverseRegistryTest.java | 10 +- 20 files changed, 1526 insertions(+), 140 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java diff --git a/docs/README_PLANETDEFS.md b/docs/README_PLANETDEFS.md index 5afda1ad1..45952fa8b 100644 --- a/docs/README_PLANETDEFS.md +++ b/docs/README_PLANETDEFS.md @@ -110,10 +110,30 @@ this file names. | attribute | unit | default | meaning | |---|---|---|---| -| `density` | 0..1 | `0.35` | Chance that a given cube of space holds a system, before the void mask. Clamped; `NaN` reads as `0`. | +| `density` | 0..1 | `0.35` | Chance that a given cube of space holds a system **at a galaxy's densest point**. Everywhere else the galaxy's own profile scales it down, and outside every galaxy it is zero. Clamped; `NaN` reads as `0`. | | `minSpacing` | cells | `40018890` | Edge of the cube that holds **at most one** system, i.e. how far apart stars stand. The default is 4.23 light years. Floors at 1. | -| `clusterScale` | super-cells | `16` | Resolution of the coarse field that separates populated space from void. Floors at 1. | -| `voidFraction` | 0..1 | `0.6` | Fraction of space that is empty. `1.0` yields an empty galaxy. Clamped; `NaN` reads as `0`. | +| `galaxySpacing` | cells | `709554785444` | Edge of the cube that holds **at most one galaxy**. The default is 75 000 light years — twenty-five galaxy diameters. Floors at 1. | +| `galaxyDensity` | 0..1 | `0.5` | Fraction of those cubes that actually hold a galaxy. The rest is intergalactic void. Clamped; `NaN` reads as `0`. | + +### Where the stars are: galaxies, not a fog + +Space is laid out twice over, by the same scheme at two scales. `galaxySpacing`-cubes hold **at most +one galaxy each**, and a galaxy is a real object: a centre, a type, a radius, an orientation, a +central bulge and — if its type has them — spiral arms. Inside it, `minSpacing`-cubes hold at most +one system each, and whether a given cube holds one is `density` **scaled by the galaxy's own profile +at that point**. So the star field thins outwards, thins away from the disc's plane, and stops at the +galaxy's edge. + +A galaxy's **type decides its size**, never the other way round: dwarf spheroidals and dwarf +irregulars outnumber spirals and ellipticals by roughly two orders, so finding a spiral is an event. +The archetype table is built in and is not authorable yet. + +**The galaxy at the origin always exists.** Authored `` anchors are absolute +coordinates, and a galaxy fills a ten-thousandth of its own cube — so without a reserved home the +system you write in this file would land in intergalactic space on virtually every seed. The home +galaxy is centred on the origin and is always drawn large enough (at least 800 light years) to hold +authored content; only its *existence* and its centre are fixed, so its type, size, orientation and +arms still differ from seed to seed. ### `` — the archetype table @@ -145,7 +165,7 @@ and below about 8 cells only the star survives. ### Changing a `` parameter mid-save is UNDEFINED -`density`, `minSpacing`, `clusterScale` and `voidFraction` are inputs to a **derived** universe: +`density`, `minSpacing`, `galaxySpacing` and `galaxyDensity` are inputs to a **derived** universe: nothing about a procedural system is stored, so changing any of them relocates every star, every planet and every generated name. **You get a different universe, and anything a player recorded about the old one — coordinates, memory crystals, a route — points at nothing.** @@ -648,7 +668,7 @@ A procedural galaxy with two archetypes and one type: ```xml - + diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index b549a6cb9..27c532d22 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -4740,12 +4740,16 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] send(sender, out.toString()); return; } - // gen-install [seed]: install a procedural - // galaxy generator and bind a seed. A world with no in its planetDefs runs the - // authored-anchors-only default, so without this there are no procedural systems to realize at - // all and every test about them would be a test about an empty universe. `gen-reset` puts the - // default back; a shared-server class MUST call it, because the generator is a JVM global. - if (args.length >= 5 && "gen-install".equalsIgnoreCase(args[0])) { + // gen-install [seed]: install a procedural galaxy generator and bind a + // seed. A world with no in its planetDefs runs the authored-anchors-only default, so + // without this there are no procedural systems to realize at all and every test about them would + // be a test about an empty universe. `gen-reset` puts the default back; a shared-server class + // MUST call it, because the generator is a JVM global. + // + // The GALAXY lattice keeps its shipped parameters. A caller near the origin is inside the home + // galaxy's core, where the profile is at its densest, so alone says how full the sky + // is — which is the one thing a test about procedural systems is actually asking for. + if (args.length >= 3 && "gen-install".equalsIgnoreCase(args[0])) { zmaster587.advancedRocketry.universe.UniverseRegistry reg = zmaster587.advancedRocketry.universe.UniverseRegistry.get(server); if (reg == null) { @@ -4754,13 +4758,13 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] } double density = parseDoubleOr(args[1], 0.9d); int minSpacing = parseIntOr(args[2], 8); - int clusterScale = parseIntOr(args[3], 8); - double voidFraction = parseDoubleOr(args[4], 0d); - long seed = args.length >= 6 ? parseLongOr(args[5], 0L) : reg.worldSeed(); + long seed = args.length >= 4 ? parseLongOr(args[3], 0L) : reg.worldSeed(); + zmaster587.advancedRocketry.universe.GalaxyGenConfig genDefaults = + zmaster587.advancedRocketry.universe.GalaxyGenConfig.defaults(); zmaster587.advancedRocketry.universe.UniverseRegistry.setGenerator( new zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator( - new zmaster587.advancedRocketry.universe.GalaxyGenConfig(density, minSpacing, - clusterScale, voidFraction, null))); + new zmaster587.advancedRocketry.universe.GalaxyGenConfig(minSpacing, density, + genDefaults.galaxySpacing, genDefaults.galaxyDensity, null, null))); reg.bindWorldSeed(seed); send(sender, "{\"ok\":true,\"seed\":" + seed + ",\"minSpacing\":" + minSpacing + "}"); return; @@ -4770,9 +4774,15 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] send(sender, "{\"ok\":true}"); return; } - // find-procedural : the first cell in a box around the origin that holds a body a ship - // could land on but that has NO dimension yet — the precondition of every realization test, and - // the thing that is impossible to write down as a literal because it depends on the seed. + // find-procedural : the first body a ship could land on that has NO dimension + // yet — the precondition of every realization test, and the thing that is impossible to write + // down as a literal because it depends on the seed. + // + // THE SWEEP IS BY SUPER-CELL, NEVER BY CELL. It used to walk raw cells around the origin, which + // worked only while a system's extent was a fraction of the star spacing. A body now stands + // where its own orbit puts it — one AU is about 150 cells — so a body is hundreds to thousands + // of cells from its star, and a box of a few cells around the origin contains nothing whatever + // the galaxy holds. Each probe asks the registry for the WHOLE system its super-cell belongs to. if (args.length >= 2 && "find-procedural".equalsIgnoreCase(args[0])) { zmaster587.advancedRocketry.universe.UniverseRegistry reg = zmaster587.advancedRocketry.universe.UniverseRegistry.get(server); @@ -4781,16 +4791,23 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] return; } long r = parseIntOr(args[1], 8); + long s = Math.max(1L, zmaster587.advancedRocketry.universe.UniverseRegistry.generator() + .minSpacingCells()); for (long x = -r; x <= r; x++) { for (long y = -r; y <= r; y++) { for (long z = -r; z <= r; z++) { - zmaster587.advancedRocketry.space.GalacticCoord cell = - zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal(x, y, z, - 0L, 0L, 0L); - for (zmaster587.advancedRocketry.universe.SystemBody b : reg.bodiesAt(cell)) { + zmaster587.advancedRocketry.space.GalacticCoord probe = + zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal( + x * s, y * s, z * s, 0L, 0L, 0L); + for (zmaster587.advancedRocketry.universe.SystemBody b + : reg.systemBodiesAt(probe)) { if (b.kind().canDescend() && b.dimId() == zmaster587.advancedRocketry.api.Constants.INVALID_PLANET) { - send(sender, "{\"ok\":true,\"sx\":" + x + ",\"sy\":" + y + ",\"sz\":" + z + // The BODY's own cell, not the probe's: that is the address every + // follow-up verb (cell-info, derived, realize) is aimed at. + zmaster587.advancedRocketry.space.GalacticCoord cell = b.name(); + send(sender, "{\"ok\":true,\"sx\":" + cell.sectorX() + ",\"sy\":" + + cell.sectorY() + ",\"sz\":" + cell.sectorZ() + ",\"cellKey\":\"" + cell.cellKey() + "\",\"kind\":\"" + b.kind() + "\",\"orbitalDist\":" + b.orbitalDistance() + ",\"starId\":" + b.starId() + "}"); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 11dec3513..5ffe254f7 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -24,14 +24,20 @@ *

Every answer is a pure function of {@code (seed, cell)} — no state, no RNG — so a scan and a later jump * agree and a re-materialised cell regenerates identically. The scheme, all O(1) per query:

*
    + *
  1. {@link GalaxyField} seats the GALAXIES — one per {@link GalaxyGenConfig#galaxySpacing}-cube, each + * with a centre, a type, a radius, an orientation and a density profile;
  2. *
  3. partition space into {@link GalaxyGenConfig#minSpacing}-cube super-cells — at most one system * each (the minimum-spacing guarantee);
  4. - *
  5. a coarse blob field grouped {@link GalaxyGenConfig#clusterScale} super-cells wide masks - * galaxy from void ({@link GalaxyGenConfig#voidFraction});
  6. - *
  7. inside a galaxy, a super-cell hosts a system with probability {@link GalaxyGenConfig#density}, seated - * at a hash-chosen cell within the super-cell.
  8. + *
  9. a super-cell hosts a system with probability {@link GalaxyGenConfig#density} scaled by the + * owning galaxy's profile at that point, seated at a hash-chosen cell within the super-cell. + * Outside every galaxy the profile is zero, so the intergalactic void is what the profile leaves + * empty rather than a second rule.
  10. *
* + *

The galaxy tier replaces an independent per-blob Bernoulli mask. Drawn per cell above the + * site-percolation threshold, that mask produced one unbounded sponge rather than galaxies: no centre, + * no radius, no orientation, and no answer to which galaxy a point was in.

+ * *

A procedural system is one or more stars (type and size sampled by weight from the seed) with * synthetic negative ids — never in the catalogue and never a dimension, so an id cannot collide * with a real star id ({@code 0..N}) or a dim id. About half of systems hold a companion, and a @@ -40,9 +46,11 @@ */ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { - // Distinct salts so the independent hash draws (blob mask, occupancy, per-axis offset, star type/size/id) - // never correlate with each other. - private static final long SALT_BLOB = 0x1L; + // Distinct salts so the independent hash draws (occupancy, per-axis offset, star type/size/id) never + // correlate with each other. + // 0x1 was SALT_BLOB, the galaxy-vs-void blob mask. Retired: which galaxy a super-cell is in, and + // how dense that galaxy is there, is now GalaxyField's answer. The number stays burned so a future + // draw cannot silently inherit an old galaxy's stream. private static final long SALT_OCC = 0x2L; private static final long SALT_OX = 0x3L; private static final long SALT_OY = 0x4L; @@ -156,10 +164,12 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { private static final double PROC_DISK_FRACTION = 0.1d; private final GalaxyGenConfig config; + private final GalaxyField galaxies; private final long totalStarWeight; public ClusteredGalaxyGenerator(GalaxyGenConfig config) { this.config = (config == null) ? GalaxyGenConfig.defaults() : config; + this.galaxies = new GalaxyField(this.config); long w = 0L; // accumulate in long so a few near-Integer.MAX weights cannot overflow the sum for (GalaxyGenConfig.StarType t : this.config.starTypes) { w += t.weight; @@ -171,6 +181,11 @@ public GalaxyGenConfig config() { return config; } + /** The galaxies this generator places its systems in — the tier above the star lattice. */ + public GalaxyField galaxies() { + return galaxies; + } + @Override public Optional systemAt(long seed, GalacticCoord coord) { long sx = coord.sectorX(); @@ -529,17 +544,22 @@ private static long clampAxis(long sector, long anchorSector, long s, long margi /** The single system a super-cell hosts (its cell coordinate + fabricated system), or empty. */ private Optional systemForSuperCell(long seed, long supX, long supY, long supZ) { - long cs = config.clusterScale; - // Void mask: a super-cell whose blob is below the void fraction hosts nothing. - double blob = CellHash.norm(CellHash.of(seed, Math.floorDiv(supX, cs), Math.floorDiv(supY, cs), - Math.floorDiv(supZ, cs), SALT_BLOB)); - if (blob < config.voidFraction) { - return Optional.empty(); + long s = config.minSpacing; + // OCCUPANCY IS DECIDED IN THE GALAXY'S OWN FRAME, so the profile does the drawing: the disc, + // the bulge and the arms place the stars. An independent per-cell draw could only ever produce + // a uniform fog, which is what made "which galaxy is this?" a question with no answer. + // + // Evaluated at the super-cell's CENTRE — a point fixed by the partition, not by any draw, so + // the probability a cube is occupied cannot depend on where its seat would have landed. And + // evaluated at t = 0 and never again: a time-dependent occupancy would pop systems in and out + // of existence. Systems drift afterwards at their galaxy's own omega(r), which is the shear. + double profile = galaxyProfileAt(seed, supX * s + s / 2L, supY * s + s / 2L, supZ * s + s / 2L); + if (!(profile > 0d)) { + return Optional.empty(); // intergalactic void, or past this galaxy's edge } - if (CellHash.norm(CellHash.of(seed, supX, supY, supZ, SALT_OCC)) >= config.density) { + if (CellHash.norm(CellHash.of(seed, supX, supY, supZ, SALT_OCC)) >= config.density * profile) { return Optional.empty(); } - long s = config.minSpacing; // Seat the anchor anywhere in its cube except a declared margin at the faces. That margin is // the system's own CLEAR SPACE, not a fraction of the cube: it is what guarantees two stars // never stand closer than the separation floor, and what keeps one system's named bodies from @@ -560,6 +580,21 @@ private Optional systemForSuperCell(long seed, long supX, long supY, return Optional.of(new Generated(cell, fabricate(seed, supX, supY, supZ))); } + /** + * How dense the owning galaxy is at this sector triple, in {@code [0, 1]} — zero in the void and + * zero past a galaxy's declared edge. + * + *

The galaxy cell is a coarse reading of the sector, so this is O(1) and needs no stored index: + * every point belongs to exactly one galaxy cell, and that cell either holds a galaxy or is void.

+ */ + private double galaxyProfileAt(long seed, long sectorX, long sectorY, long sectorZ) { + Optional galaxy = galaxies.galaxyOwningSector(seed, sectorX, sectorY, sectorZ); + if (!galaxy.isPresent()) { + return 0d; + } + return galaxy.get().densityAtSector(sectorX, sectorY, sectorZ); + } + private StarSystem fabricate(long seed, long supX, long supY, long supZ) { GalaxyGenConfig.StarType type = pickType(CellHash.of(seed, supX, supY, supZ, SALT_TYPE)); double sizeFrac = CellHash.norm(CellHash.of(seed, supX, supY, supZ, SALT_SIZE)); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java new file mode 100644 index 000000000..a7fbacf38 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java @@ -0,0 +1,273 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.space.GalacticCoord; + +/** + * One galaxy: a seated object with a centre, a type, a size, an orientation and a density profile. + * + *

It is a VALUE, produced on demand from {@code (seed, galaxy cell)} and stored nowhere — exactly + * as a {@link StarSystem} is. Nothing here is persisted and no coordinate carries a galaxy index; the + * index is {@code sector / galaxySpacing}, a derived grouping of the sector space that already + * exists (see {@link GalaxyField#galaxyIndex}).

+ * + *

What a galaxy is FOR

+ *
    + *
  • It draws the star field. A super-cell hosts a system with a probability scaled by + * {@link #densityAt} at that point, so the disc, the bulge and the arms place the stars + * instead of an independent per-cell coin toss.
  • + *
  • It is the frame a bound thing rides. Inside the declared {@link #radiusLy radius} a + * position co-rotates at {@link #angularSpeedAt}; outside it, it does not.
  • + *
+ * + *

The inside/outside test is the DECLARED RADIUS, never a level of the profile. A profile is + * continuous and has no boundary, so a frame decided by "is the density high enough here" would flip + * back and forth for anything hovering near the threshold. The radius is a sphere: the disc and its + * halo are both inside it, which is right — a halo is bound to its galaxy too.

+ * + *

Rotation

+ *

{@code θ(t) = θ₀ + ω(r)·t}, analytic in {@code t} and never integrated, so nothing accumulates + * drift — the argument {@link BodyEphemeris} already makes one level down. The curve is + * {@code v(r) = v∞ · r / √(r² + r_core²)}: solid-body near the centre, flat outside the core, and the + * type's {@link GalaxyGenConfig.GalaxyType#coreRadiusFraction} says where the turnover is, so a dwarf + * rotates almost rigidly (little shear) and a massive spiral shears strongly. Hence + * {@code ω(r) = v∞ / √(r² + r_core²)}, which is finite at the centre rather than singular.

+ * + *

The rate is slow enough to be invisible inside one save, which is the ratified position: the + * mechanic exists even when slow, and the speed is tuning.

+ */ +public final class Galaxy { + + /** How far the exponential disc reaches, as a fraction of the radius. */ + private static final double DISC_SCALE_FRACTION = 1d / 3d; + /** How far the central bulge reaches, as a fraction of the radius. */ + private static final double BULGE_SCALE_FRACTION = 1d / 12d; + /** How strongly the arms modulate the disc: the density between arms against the density on one. */ + private static final double ARM_CONTRAST = 0.6d; + /** The centre is a singular point of the arm winding; inside this fraction the bulge speaks. */ + private static final double ARM_INNER_FRACTION = 1e-3d; + + private final long cellX; + private final long cellY; + private final long cellZ; + private final GalacticCoord centre; + private final GalaxyGenConfig.GalaxyType type; + private final double radiusLy; + private final double armPitch; + private final double armPhase; + + // The galaxy frame, precomputed: (u, v) span its plane and w is its pole. A position's cylindrical + // (r, theta, z) is read off these, so the profile below is written in the galaxy's own terms and + // the orientation is applied exactly once. + private final double ux; + private final double uy; + private final double uz; + private final double vx; + private final double vy; + private final double vz; + private final double wx; + private final double wy; + private final double wz; + + /** + * @param cellX the galaxy-lattice index this galaxy is seated in + * @param centre its centre, as a cell name + * @param radiusLy its declared radius in light years — drawn inside {@code type}'s band + * @param tilt the angle its pole makes with the static +Y axis, in radians + * @param node the direction that pole leans in, in radians about +Y + * @param armPitch the arms' pitch angle in radians (ignored when the type has no arms) + * @param armPhase where arm zero starts, in radians + */ + public Galaxy(long cellX, long cellY, long cellZ, GalacticCoord centre, + GalaxyGenConfig.GalaxyType type, double radiusLy, double tilt, double node, + double armPitch, double armPhase) { + this.cellX = cellX; + this.cellY = cellY; + this.cellZ = cellZ; + this.centre = centre; + this.type = type; + this.radiusLy = Math.max(1d, radiusLy); + this.armPitch = armPitch; + this.armPhase = armPhase; + + double st = Math.sin(tilt); + double ct = Math.cos(tilt); + double sn = Math.sin(node); + double cn = Math.cos(node); + // w = the pole; u, v = an orthonormal pair spanning the plane it is normal to. + this.wx = st * cn; + this.wy = ct; + this.wz = st * sn; + this.ux = ct * cn; + this.uy = -st; + this.uz = ct * sn; + this.vx = -sn; + this.vy = 0d; + this.vz = cn; + } + + public long cellX() { + return cellX; + } + + public long cellY() { + return cellY; + } + + public long cellZ() { + return cellZ; + } + + /** Where this galaxy's centre stands, as a cell name. */ + public GalacticCoord centre() { + return centre; + } + + public GalaxyGenConfig.GalaxyType type() { + return type; + } + + /** The declared radius in light years — the boundary, and the only boundary. */ + public double radiusLy() { + return radiusLy; + } + + /** The arms' pitch angle in radians; meaningless when the type has no arms. */ + public double armPitch() { + return armPitch; + } + + /** Where arm zero starts, in radians. */ + public double armPhase() { + return armPhase; + } + + /** This galaxy's designation — procedurally-generated galaxy, named for the cell it is seated in. */ + public String name() { + return "PGG-" + cellX + "." + cellY + "." + cellZ; + } + + // ─── Membership and profile ──────────────────────────────────────────────── + + /** Whether a point {@code (dx, dy, dz)} light years from the centre is inside this galaxy. */ + public boolean contains(double dxLy, double dyLy, double dzLy) { + return dxLy * dxLy + dyLy * dyLy + dzLy * dzLy <= radiusLy * radiusLy; + } + + /** Whether a cell named by this sector triple is inside this galaxy. */ + public boolean containsSector(long sectorX, long sectorY, long sectorZ) { + double dx = offsetLy(sectorX, centre.sectorX()); + double dy = offsetLy(sectorY, centre.sectorY()); + double dz = offsetLy(sectorZ, centre.sectorZ()); + return contains(dx, dy, dz); + } + + /** + * How dense this galaxy is at a point {@code (dx, dy, dz)} light years from its centre, as a + * fraction of its densest point: {@code 0} outside the radius, {@code 1} at the nucleus. + * + *

This is the ONE function that decides both where stars are placed and what shape a galaxy + * reads as. A disc is an exponential disc times an exponential in height, modulated by arms and + * added to a bulge; a spheroid is one isotropic exponential with the type's flattening applied to + * its pole.

+ */ + public double densityAt(double dxLy, double dyLy, double dzLy) { + if (!contains(dxLy, dyLy, dzLy)) { + return 0d; + } + // Into the galaxy's own frame: the plane it spans, and the height above it. + double localX = dxLy * ux + dyLy * uy + dzLy * uz; + double localY = dxLy * vx + dyLy * vy + dzLy * vz; + double z = dxLy * wx + dyLy * wy + dzLy * wz; + double r = Math.hypot(localX, localY); + + if (type.profile == GalaxyGenConfig.GalaxyProfile.SPHEROID) { + // Round, with the type's flattening squashing the pole. No plane, so no arms and no bulge + // term — the whole thing IS the bulge. + double scaled = Math.hypot(r, z / Math.max(1e-6d, type.scaleHeightRatio)); + return clamp01(Math.exp(-scaled / (radiusLy * DISC_SCALE_FRACTION))); + } + + double scaleHeight = Math.max(1e-6d, radiusLy * type.scaleHeightRatio); + double disc = Math.exp(-r / (radiusLy * DISC_SCALE_FRACTION)) + * Math.exp(-Math.abs(z) / scaleHeight); + disc *= armFactor(r, Math.atan2(localY, localX)); + double bulge = Math.exp(-Math.hypot(r, z) / (radiusLy * BULGE_SCALE_FRACTION)); + return clamp01(disc + bulge); + } + + /** The profile read at a cell name — the form the generator asks in. */ + public double densityAtSector(long sectorX, long sectorY, long sectorZ) { + return densityAt(offsetLy(sectorX, centre.sectorX()), + offsetLy(sectorY, centre.sectorY()), + offsetLy(sectorZ, centre.sectorZ())); + } + + /** + * The arms' contribution as a multiplier in {@code (0, 1]}, normalised so a point ON an arm scores + * 1 and the disc between them is dimmer. A type with no arms scores 1 everywhere, so a smooth disc + * is the same code path with an empty term rather than a branch somewhere else. + */ + private double armFactor(double r, double theta) { + if (type.armCount <= 0) { + return 1d; + } + double tan = Math.tan(armPitch); + if (!(Math.abs(tan) > 1e-9d)) { + return 1d; // a degenerate pitch would wind the arms into a circle; leave the disc smooth + } + double rArm = Math.max(r, radiusLy * ARM_INNER_FRACTION); + double wind = Math.log(rArm / radiusLy) / tan; + double phase = type.armCount * (theta - armPhase - wind); + return (1d + ARM_CONTRAST * Math.cos(phase)) / (1d + ARM_CONTRAST); + } + + // ─── Rotation ────────────────────────────────────────────────────────────── + + /** + * The angular speed at galaxy-local radius {@code rLy}, in radians per tick — the SHEAR that makes + * a galaxy a place that moves rather than a fixed backdrop. + * + *

Signed: the sign is the galaxy's spin direction about its own pole, and it is the same + * everywhere in one galaxy. Positive always here; the pole's direction is what distinguishes two + * galaxies spinning opposite ways, and that is carried by the orientation.

+ */ + public double angularSpeedAt(double rLy) { + double core = radiusLy * type.coreRadiusFraction; + double speed = UniverseScale.lightYearsPerTick(type.rotationSpeedKmS); + return speed / Math.hypot(Math.max(0d, rLy), core); + } + + /** + * Where something that started at {@code theta0} and sits at radius {@code rLy} has got to by tick + * {@code tick}. Evaluated, never integrated. + */ + public double thetaAt(double theta0, double rLy, long tick) { + return theta0 + angularSpeedAt(rLy) * (double) tick; + } + + /** How long one turn at radius {@code rLy} takes, in ticks. Diagnostics and tests read this. */ + public double rotationPeriodTicks(double rLy) { + double omega = angularSpeedAt(rLy); + return omega > 0d ? 2d * Math.PI / omega : Double.POSITIVE_INFINITY; + } + + // ─── Helpers ─────────────────────────────────────────────────────────────── + + /** A sector delta as a length in light years. Exact: the delta is bounded by one galaxy cell. */ + private static double offsetLy(long sector, long centreSector) { + return UniverseScale.lightYearsForCells((double) (sector - centreSector)); + } + + private static double clamp01(double v) { + if (!(v > 0d)) { + return 0d; + } + return v > 1d ? 1d : v; + } + + @Override + public String toString() { + return "Galaxy[" + name() + " " + type.name + " r=" + (long) radiusLy + "ly centre=" + + centre.cellKey() + "]"; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java new file mode 100644 index 000000000..298f20f38 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java @@ -0,0 +1,242 @@ +package zmaster587.advancedRocketry.universe; + +import java.util.Optional; + +import zmaster587.advancedRocketry.space.GalacticCoord; + +/** + * Where the galaxies are: the lattice one level above the star lattice, and the same scheme. + * + *

Space is partitioned into {@code galaxySpacing}-cube galaxy cells; a cell holds at most + * one galaxy, seated at a hash offset inside it, with every parameter — type, radius, orientation, + * arms — drawn from {@code hash(seed, gx, gy, gz)}. Nothing is stored. A galaxy is a value produced + * by this class exactly as a system is produced by {@link ClusteredGalaxyGenerator}.

+ * + *

The galaxy index is DERIVED, not an addressing tier

+ *

{@link #galaxyIndex} is {@code sector / galaxySpacing} — a coarse reading of the sector space + * that already exists. No coordinate gains a field, nothing is persisted, and a distance is still a + * distance. That is what makes "which galaxy is this?" an O(1) question with an answer, where an + * independent per-cell mask left it undefined.

+ * + *

Every point is in a galaxy CELL; only some are in a GALAXY

+ *

There is no "nowhere". A cell either holds a galaxy or is entirely void, and inside a cell that + * holds one, a point is inside the galaxy iff it is within the declared radius. Those are two + * different questions and they have two different methods here — {@link #galaxyOwning} names the + * cell's galaxy, {@link Galaxy#containsSector} says whether you are in it.

+ * + *

The home galaxy

+ *

Galaxy cell {@code (0,0,0)} is RESERVED: it always holds a galaxy, centred on the origin, drawn + * only among types large enough to hold authored content. A galaxy is otherwise a hash draw and may + * simply not be there under another seed — but authored content must exist under EVERY seed, and a + * hand-picked absolute coordinate would otherwise land in intergalactic space with probability + * 99.997 %. Only its EXISTENCE and its centre are fixed; its type, size, orientation and arms are + * drawn like any other galaxy's, so every world's home galaxy is still its own.

+ */ +public final class GalaxyField { + + // A salt space of its own, well clear of the generator's, so a galaxy draw and a star draw over + // the same integer triple can never be the same number. + private static final long SALT_GALAXY_OCC = 0x101L; + private static final long SALT_GALAXY_TYPE = 0x102L; + private static final long SALT_GALAXY_RADIUS = 0x103L; + private static final long SALT_GALAXY_OX = 0x104L; + private static final long SALT_GALAXY_OY = 0x105L; + private static final long SALT_GALAXY_OZ = 0x106L; + private static final long SALT_GALAXY_TILT = 0x107L; + private static final long SALT_GALAXY_NODE = 0x108L; + private static final long SALT_GALAXY_PITCH = 0x109L; + private static final long SALT_GALAXY_PHASE = 0x10AL; + + /** Arms are drawn in this pitch band, in degrees — the range real spirals occupy. */ + private static final double MIN_ARM_PITCH_DEGREES = 10d; + private static final double MAX_ARM_PITCH_DEGREES = 30d; + + private final GalaxyGenConfig config; + private final long totalGalaxyWeight; + private final long totalHomeWeight; + + public GalaxyField(GalaxyGenConfig config) { + this.config = (config == null) ? GalaxyGenConfig.defaults() : config; + long all = 0L; // accumulated in long so a few near-Integer.MAX weights cannot overflow the sum + long home = 0L; + for (GalaxyGenConfig.GalaxyType t : this.config.galaxyTypes) { + all += t.weight; + if (qualifiesAsHome(t)) { + home += t.weight; + } + } + this.totalGalaxyWeight = Math.max(1L, all); + this.totalHomeWeight = home; + } + + public GalaxyGenConfig config() { + return config; + } + + /** + * The galaxy-lattice index a sector belongs to: the DERIVED grouping that answers "which galaxy + * cell is this", with nothing stored anywhere. + * + *

The lattice is offset by half a cell, so the ORIGIN is a cell CENTRE and not a corner. + * That is what lets the home galaxy be centred on the origin and still sit wholly inside its own + * cell — with the corner convention, every sector with a negative coordinate would belong to a + * NEIGHBOURING cell, so most of the space around the shipped solar system would have been reading + * a different galaxy's profile (or none) while standing inside the home galaxy.

+ * + *

The half-cell shift is applied to the QUOTIENT rather than to the coordinate: adding it to a + * sector near the {@code long} limit would overflow, and a coordinate that silently wraps is + * exactly the failure this layer removed from {@code absoluteX()}.

+ */ + public static long galaxyIndex(long sector, long galaxySpacing) { + long s = Math.max(1L, galaxySpacing); + long half = s / 2L; + long rem = Math.floorMod(sector, s); + long base = Math.floorDiv(sector, s); + return rem >= s - half ? base + 1L : base; + } + + /** The lowest sector belonging to galaxy cell {@code index} on one axis. */ + public static long cellLowCorner(long index, long galaxySpacing) { + long s = Math.max(1L, galaxySpacing); + return index * s - s / 2L; + } + + /** + * The galaxy whose CELL contains this sector triple, or empty when that cell is void. It does not + * ask whether the point is inside the galaxy — see {@link Galaxy#containsSector} for that. + */ + public Optional galaxyOwningSector(long seed, long sectorX, long sectorY, long sectorZ) { + long s = config.galaxySpacing; + return galaxyAtIndex(seed, galaxyIndex(sectorX, s), galaxyIndex(sectorY, s), + galaxyIndex(sectorZ, s)); + } + + /** The galaxy whose cell contains {@code cell}, or empty when that cell is void. */ + public Optional galaxyOwning(long seed, GalacticCoord cell) { + return galaxyOwningSector(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ()); + } + + /** The home galaxy — the one authored content lives in. Present under every seed, by construction. */ + public Galaxy home(long seed) { + // Reserved, so the Optional is always full; unwrapping it here is what makes that a statement + // callers can rely on rather than one they have to re-check. + return galaxyAtIndex(seed, 0L, 0L, 0L).get(); + } + + /** Whether this galaxy-lattice index is the reserved home cell. */ + public static boolean isHomeCell(long gx, long gy, long gz) { + return gx == 0L && gy == 0L && gz == 0L; + } + + /** + * The galaxy seated in galaxy cell {@code (gx, gy, gz)}, or empty when the cell is void. + * + *

Every parameter is a hash draw over the cell index, so the answer is a pure function of + * {@code (seed, cell)} and two queries about the same galaxy can never disagree.

+ */ + public Optional galaxyAtIndex(long seed, long gx, long gy, long gz) { + boolean home = isHomeCell(gx, gy, gz); + if (!home && !occupied(seed, gx, gy, gz)) { + return Optional.empty(); + } + GalaxyGenConfig.GalaxyType type = pickType(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_TYPE), + home); + double radiusFraction = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_RADIUS)); + double radiusLy = type.minRadiusLy + radiusFraction * (type.maxRadiusLy - type.minRadiusLy); + + // An isotropic pole: cos(tilt) uniform, not tilt uniform, or galaxies would cluster edge-on. + double tilt = Math.acos(2d * CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_TILT)) - 1d); + double node = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_NODE)) * 2d * Math.PI; + double pitch = Math.toRadians(MIN_ARM_PITCH_DEGREES + + CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_PITCH)) + * (MAX_ARM_PITCH_DEGREES - MIN_ARM_PITCH_DEGREES)); + double phase = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_PHASE)) * 2d * Math.PI; + + return Optional.of(new Galaxy(gx, gy, gz, seatOf(seed, gx, gy, gz, radiusLy, home), type, + radiusLy, tilt, node, pitch, phase)); + } + + /** + * Whether this cell holds a galaxy at all. + * + *

The cosmic-web slot. Galaxies in reality lie on filaments around genuine voids, and + * that is a field over the lattice, not a per-cell coin toss. The field is not built — none of its + * numbers is ratified and it needs spatially CORRELATED noise, a primitive this generator does not + * have. What is built is the shape it drops into: {@link #webDensity} is the constant 1 today and + * becomes that field later, with no change to placement.

+ */ + private boolean occupied(long seed, long gx, long gy, long gz) { + double threshold = config.galaxyDensity * webDensity(gx, gy, gz); + return CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_OCC)) < threshold; + } + + /** + * How much likelier than baseline a galaxy is at this lattice index — the cosmic web's hook. It is + * deliberately a constant: this states that galaxy density is not REQUIRED to be uniform, and + * names the one place non-uniformity will live. + */ + static double webDensity(long gx, long gy, long gz) { + return 1d; + } + + /** + * Where the galaxy sits inside its cube: anywhere that leaves it wholly inside, so it never + * straddles a face. + * + *

That containment is what keeps three things true at once — at most one galaxy per cell, + * galaxies that cannot overlap, and an O(1) answer to "which galaxy is this point in" that reads + * the containing cell and nothing else.

+ * + *

The home galaxy is centred on the ORIGIN instead. Authored anchors are declared in absolute + * coordinates today, so the origin is where authored content actually is; seating the home galaxy + * anywhere else would put the shipped solar system in intergalactic space.

+ */ + private GalacticCoord seatOf(long seed, long gx, long gy, long gz, double radiusLy, boolean home) { + if (home) { + return GalacticCoord.ORIGIN; + } + long s = config.galaxySpacing; + long margin = Math.min(UniverseScale.cellsForLightYears(radiusLy), Math.max(0L, (s - 1L) / 2L)); + long band = Math.max(1L, s - 2L * margin); + // The index came from a real sector, so a cell corner is bounded by that sector and the + // products below cannot overflow: each is at most the coordinate it was derived from. + long ox = margin + Math.floorMod(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_OX), band); + long oy = margin + Math.floorMod(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_OY), band); + long oz = margin + Math.floorMod(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_OZ), band); + return GalacticCoord.ofSectorLocal(cellLowCorner(gx, s) + ox, cellLowCorner(gy, s) + oy, + cellLowCorner(gz, s) + oz, 0L, 0L, 0L); + } + + /** + * Whether a type may be drawn for the HOME galaxy: its smallest possible radius must already + * clear the guaranteed minimum, so the guarantee is a constraint on the DRAW rather than a clamp + * applied to its result. + */ + private static boolean qualifiesAsHome(GalaxyGenConfig.GalaxyType type) { + return type.minRadiusLy >= UniverseScale.MIN_HOME_GALAXY_RADIUS_LY; + } + + /** + * Draw a type by weight — over the whole table, or over the subset a home galaxy may be. + * + *

A table with nothing large enough to be a home falls back to the whole table: a pack that + * ships only dwarf galaxies gets the universe it asked for, and its authored content had better + * be close to the centre.

+ */ + private GalaxyGenConfig.GalaxyType pickType(long h, boolean home) { + boolean restricted = home && totalHomeWeight > 0L; + long r = Math.floorMod(h, restricted ? totalHomeWeight : totalGalaxyWeight); + GalaxyGenConfig.GalaxyType last = null; + for (GalaxyGenConfig.GalaxyType t : config.galaxyTypes) { + if (restricted && !qualifiesAsHome(t)) { + continue; + } + last = t; + if (r < t.weight) { + return t; + } + r -= t.weight; + } + return last; // config.galaxyTypes is never empty + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index 09cbc2783..446fc97e0 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -9,10 +9,19 @@ * knobs, never a contract — authored via the optional {@code } XML element; every field has a * default so {@code } with no attributes is valid. * - *

Immutable. The distribution is CLUSTERED: space is partitioned into {@link #minSpacing}-cube - * "super-cells" (at most one system each — the spacing guarantee), and a coarser blob field grouped - * {@link #clusterScale} super-cells wide decides which super-cells sit inside a galaxy versus the - * inter-galaxy {@link #voidFraction void}.

+ *

Immutable, and it describes TWO nested lattices of the same shape:

+ *
    + *
  • {@link #galaxySpacing}-cube galaxy cells, at most one galaxy each, occupied with + * probability {@link #galaxyDensity} — a galaxy is a seated object with a type, a radius, an + * orientation and a density profile ({@link Galaxy});
  • + *
  • {@link #minSpacing}-cube super-cells, at most one system each, occupied with + * probability {@link #density} scaled by the owning galaxy's profile at that point.
  • + *
+ * + *

The galaxy tier replaces an independent per-blob Bernoulli mask (a {@code clusterScale} field and + * a {@code voidFraction}, both retired). That mask drew each blob cell independently at a probability + * above the site-percolation threshold, so the "galaxies" it produced were one unbounded sponge: no + * centre, no radius, no orientation, and no answer to which galaxy a point is in.

*/ public final class GalaxyGenConfig { @@ -30,6 +39,15 @@ public final class GalaxyGenConfig { */ public static final int DEFAULT_MIN_SPACING = UniverseScale.DEFAULT_SPACING_CELLS; + /** + * Default galaxy-cell edge in cells — {@link UniverseScale#DEFAULT_GALAXY_SPACING_CELLS}. A + * {@code long}: the galaxy lattice is five orders coarser than the star lattice. + */ + public static final long DEFAULT_GALAXY_SPACING = UniverseScale.DEFAULT_GALAXY_SPACING_CELLS; + + /** Fraction of galaxy cells that actually hold a galaxy, before the cosmic web weights them. */ + public static final double DEFAULT_GALAXY_DENSITY = 0.5d; + /** A weighted star archetype: a temperature (drives colour) and a size range. */ public static final class StarType { public final int temperature; @@ -45,34 +63,107 @@ public StarType(int temperature, float minSize, float maxSize, int weight) { } } - /** Per-super-cell occupancy probability inside a galaxy (before the void mask). */ + /** + * The radial shape a galaxy's stars are distributed in. It decides the FORM of the profile, not + * its size: how far the stars reach is the galaxy's radius, which is drawn per type. + */ + public enum GalaxyProfile { + /** A flattened exponential disc with a central bulge, and arms when the type has them. */ + DISC, + /** A round exponential cloud — no plane, no arms, no preferred direction. */ + SPHEROID + } + + /** + * A weighted galaxy archetype. The exact analogue of {@link StarType} one level up, and it exists + * for the same reason: so that size is drawn CONDITIONAL ON TYPE, never independently. + * + *

Independent draws would produce dwarf galaxies carrying spiral arms and spirals the size of a + * dwarf — the type and the size of a real galaxy are not two facts, they are one. The weights are + * what makes "mostly dwarfs, and a spiral is a find" a property of the table rather than a rule + * somewhere in the generator.

+ */ + public static final class GalaxyType { + /** Short archetype name; a seated galaxy's designation is built from it. */ + public final String name; + public final GalaxyProfile profile; + /** Radius band, in light years. A galaxy's radius is DRAWN INSIDE ITS TYPE'S band. */ + public final double minRadiusLy; + public final double maxRadiusLy; + /** + * Scale height as a fraction of the radius — how flat the thing is. A real thin disc is about + * 1:50, an irregular is a fat slab, a spheroid is nearly round. + */ + public final double scaleHeightRatio; + /** Spiral arms, or 0 for a type that has none. */ + public final int armCount; + /** The rotation curve's asymptotic speed, in km/s — quoted the way astronomy quotes it. */ + public final double rotationSpeedKmS; + /** + * Where the rotation curve turns over, as a fraction of the radius. Near 1 the whole galaxy + * rotates almost as a solid body (little shear); near 0 the curve is flat almost everywhere + * (strong shear, and arms that wind up). + */ + public final double coreRadiusFraction; + public final int weight; + + public GalaxyType(String name, GalaxyProfile profile, double minRadiusLy, double maxRadiusLy, + double scaleHeightRatio, int armCount, double rotationSpeedKmS, + double coreRadiusFraction, int weight) { + this.name = (name == null || name.isEmpty()) ? "GALAXY" : name; + this.profile = (profile == null) ? GalaxyProfile.DISC : profile; + this.minRadiusLy = Math.max(1d, minRadiusLy); + this.maxRadiusLy = Math.max(this.minRadiusLy, maxRadiusLy); + this.scaleHeightRatio = Math.min(1d, Math.max(0.001d, scaleHeightRatio)); + this.armCount = Math.max(0, armCount); + this.rotationSpeedKmS = Math.max(0d, rotationSpeedKmS); + this.coreRadiusFraction = Math.min(1d, Math.max(0.001d, coreRadiusFraction)); + this.weight = Math.max(1, weight); + } + } + + /** + * Per-super-cell occupancy probability, before the owning galaxy's profile scales it. It is the + * density AT A GALAXY'S DENSEST POINT, not an average over space: outside a galaxy the profile is + * zero and no value here places a system. + */ public final double density; /** * Super-cell edge in cells: at most one system per {@code minSpacing}-cube, i.e. how far apart * stars stand. It bounds no orbit — see {@link #DEFAULT_MIN_SPACING}. */ public final int minSpacing; - /** Blob field resolution in super-cells — the size of a galaxy cluster. */ - public final int clusterScale; - /** Fraction of space that is inter-galaxy void (no systems). */ - public final double voidFraction; + /** Galaxy-cell edge in cells: at most one galaxy per {@code galaxySpacing}-cube. */ + public final long galaxySpacing; + /** Fraction of galaxy cells that hold a galaxy at all — the rest is intergalactic void. */ + public final double galaxyDensity; /** Star archetypes sampled by weight when a system is placed (never empty). */ public final List starTypes; + /** Galaxy archetypes sampled by weight when a galaxy is seated (never empty). */ + public final List galaxyTypes; - public GalaxyGenConfig(double density, int minSpacing, int clusterScale, double voidFraction, - List starTypes) { + /** + * Each lattice states its EDGE and then its OCCUPANCY, stars first and galaxies second, so the two + * (edge, density) pairs cannot be read for one another. + */ + public GalaxyGenConfig(int minSpacing, double density, long galaxySpacing, double galaxyDensity, + List starTypes, List galaxyTypes) { this.density = clamp01(density); this.minSpacing = Math.max(1, minSpacing); - this.clusterScale = Math.max(1, clusterScale); - this.voidFraction = clamp01(voidFraction); + this.galaxySpacing = Math.max(1L, galaxySpacing); + this.galaxyDensity = clamp01(galaxyDensity); this.starTypes = (starTypes == null || starTypes.isEmpty()) ? defaultStarTypes() : Collections.unmodifiableList(new ArrayList<>(starTypes)); + this.galaxyTypes = (galaxyTypes == null || galaxyTypes.isEmpty()) + ? defaultGalaxyTypes() + : Collections.unmodifiableList(new ArrayList<>(galaxyTypes)); } /** A sparse, strongly-clustered default galaxy. */ public static GalaxyGenConfig defaults() { - return new GalaxyGenConfig(0.35d, DEFAULT_MIN_SPACING, 16, 0.6d, defaultStarTypes()); + return new GalaxyGenConfig(DEFAULT_MIN_SPACING, 0.35d, DEFAULT_GALAXY_SPACING, + DEFAULT_GALAXY_DENSITY, defaultStarTypes(), defaultGalaxyTypes()); } private static List defaultStarTypes() { @@ -85,6 +176,21 @@ private static List defaultStarTypes() { return Collections.unmodifiableList(l); } + /** + * The stock galaxy table. Weights are the real abundance ordering — dwarfs outnumber giants by two + * orders — so a spiral is something a player FINDS rather than the default sky. + */ + private static List defaultGalaxyTypes() { + List l = new ArrayList<>(); + // name profile radius band flatten arms km/s core weight + l.add(new GalaxyType("Dwarf Spheroidal", GalaxyProfile.SPHEROID, 120d, 500d, 0.70d, 0, 20d, 0.90d, 700)); + l.add(new GalaxyType("Dwarf Irregular", GalaxyProfile.DISC, 200d, 900d, 0.30d, 0, 50d, 0.60d, 290)); + l.add(new GalaxyType("Spiral", GalaxyProfile.DISC, 900d, 2200d, 0.02d, 2, 220d, 0.08d, 7)); + l.add(new GalaxyType("Barred Spiral", GalaxyProfile.DISC, 1000d, 2500d, 0.02d, 4, 210d, 0.10d, 2)); + l.add(new GalaxyType("Elliptical", GalaxyProfile.SPHEROID, 1500d, 3500d, 0.60d, 0, 40d, 0.50d, 1)); + return Collections.unmodifiableList(l); + } + private static double clamp01(double v) { if (Double.isNaN(v) || v < 0d) { return 0d; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java index 6d7c256ec..ccc0231c2 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java @@ -102,6 +102,15 @@ public final class UniverseRegistry extends WorldSavedData implements CellFrames // ─── JVM-global seams / staging ─────────────────────────────────────────── private static volatile IGalaxyGenerator generator = new EmptyGalaxyGenerator(); + + /** + * The installed generator. The counterpart of {@link #setGenerator}: a caller that needs the + * partition the universe is laid out on — the super-cell edge, above all — has to be able to ask + * for it rather than assume a number that the configuration owns. + */ + public static IGalaxyGenerator generator() { + return generator; + } // How a stored star-id resolves to its content object. Defaults to the legacy catalogue; overridable so // the forward coord->system path is unit-testable without booting DimensionManager, and so an addon can // supply fabricated systems. diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java index 8b2c765d4..72c4a1308 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java @@ -20,6 +20,10 @@ * room a system's bodies have and how close two unrelated systems may ever be seen to stand. * * + *

One level up, the same pair says how big a galaxy is and how far apart galaxies stand — see the + * galaxy-lattice section below. It is the same scheme applied twice, which is the point: a system is + * seated in a cube, and so is the galaxy that holds it.

+ * *

These used to be one number: a system's extent was defined as a fraction of the * interstellar step, which truncated systems at a few AU, filled half the gap to the next star with * one system's neighbourhood, and forced the orbit scale to shrink to compensate. Separating them is @@ -76,9 +80,82 @@ public final class UniverseScale { Math.max(1L, Math.round(MEAN_STAR_SEPARATION_LY * AstronomicalBodyHelper.BLOCKS_PER_LIGHT_YEAR / (double) GalacticCoord.CELL))); + // ─── The galaxy lattice ──────────────────────────────────────────────────── + // One level up, and the same scheme: a cube that holds at most one galaxy, and a galaxy seated + // inside it. What is stated here is a REFERENCE SIZE and a RATIO; the separation follows from + // them, and an individual galaxy's radius is drawn per type around the reference. + // + // The reference is deliberately about a thirtieth of a real giant galaxy, and the separation is + // scaled with it so the RATIO stays real. That is what buys the whole void a single primitive: + // an offset inside one galaxy cube has to fit a long, and at real sizes it would not. + + /** + * The size a galaxy is quoted against, in light years — a mid-sized spiral, holding of the order + * of a million systems at {@link #MEAN_STAR_SEPARATION_LY}. Every type's radius band is drawn + * around it. + * + *

It is about a thirtieth of a real giant galaxy, and that is the number the whole layer is + * sized by: a position out in the void is an offset from its galaxy cell's origin, so the cell + * edge has to fit a {@code long} of blocks. At real sizes it would not, and the void would need a + * second, coarser representation of its own.

+ */ + public static final double REFERENCE_GALAXY_RADIUS_LY = 1_500d; + + /** + * How far apart galaxies stand, in galaxy DIAMETERS. This is the real number — galaxies in a + * group sit tens of diameters apart — and it is what the separation below is derived from, so + * shrinking the reference size shrinks the whole layer coherently instead of leaving galaxies + * marooned at a real separation. + */ + public static final double GALAXY_SEPARATION_IN_DIAMETERS = 25d; + + /** Edge of the cube that holds at most one galaxy, in light years. */ + public static final double MEAN_GALAXY_SEPARATION_LY = + GALAXY_SEPARATION_IN_DIAMETERS * 2d * REFERENCE_GALAXY_RADIUS_LY; + + /** + * The same edge in cells — the default {@code galaxySpacing}. A {@code long}, not an {@code int}: + * the galaxy lattice is five orders coarser than the star lattice and does not fit one. + */ + public static final long DEFAULT_GALAXY_SPACING_CELLS = + cellsForLightYears(MEAN_GALAXY_SEPARATION_LY); + + /** + * The radius the HOME galaxy is guaranteed to have at least, in light years. A galaxy's size is + * hash-drawn, so without a floor a pack that places authored content a few hundred light years + * out would work on one seed and put that content outside its own galaxy on the next. The floor + * is expressed as a constraint on which TYPES the home galaxy may be drawn from, never as a + * clamp applied afterwards. + */ + public static final double MIN_HOME_GALAXY_RADIUS_LY = 800d; + private UniverseScale() { } + /** How many cells a length in light years spans. Rounded up: a reach must not come out short. */ + public static long cellsForLightYears(double lightYears) { + double blocks = Math.max(0d, lightYears) * AstronomicalBodyHelper.BLOCKS_PER_LIGHT_YEAR; + return (long) Math.ceil(blocks / (double) GalacticCoord.CELL); + } + + /** The length in light years that {@code cells} cells span. */ + public static double lightYearsForCells(double cells) { + return cells * (double) GalacticCoord.CELL + / (double) AstronomicalBodyHelper.BLOCKS_PER_LIGHT_YEAR; + } + + /** + * A speed quoted in km/s as light years per TICK — the form an angular rate is evaluated in. + * + *

Galactic velocities are stated the way astronomy states them and converted once, here, + * rather than being pre-divided into a per-tick literal that no longer says what it measures.

+ */ + public static double lightYearsPerTick(double kilometresPerSecond) { + double metresPerYear = kilometresPerSecond * 1_000d * AstronomicalBodyHelper.SECONDS_PER_YEAR; + return metresPerYear / AstronomicalBodyHelper.METRES_PER_LIGHT_YEAR + / AstronomicalBodyHelper.TICKS_PER_YEAR; + } + /** How many cells an orbital distance spans. Rounded up: a reach must not come out short. */ public static long cellsForOrbitUnits(double orbitUnits) { double blocks = Math.max(0d, orbitUnits) * AstronomicalBodyHelper.BLOCKS_PER_ORBIT_UNIT; diff --git a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java index 44dffdfbd..d2a9cd86a 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java +++ b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java @@ -41,6 +41,11 @@ public class AstronomicalBodyHelper { public static final double METRES_PER_AU = 1.495_978_707e11d; /** Metres in one Julian light year. */ public static final double METRES_PER_LIGHT_YEAR = 9.460_730_472_580_8e15d; + /** + * Seconds in one Julian year. What carries a speed stated per SECOND — the unit orbital and + * galactic velocities are quoted in — into the per-year frame the calendar below counts in. + */ + public static final double SECONDS_PER_YEAR = 31_557_600d; /** Chart blocks in one astronomical unit. */ public static final long BLOCKS_PER_AU = @@ -92,6 +97,12 @@ public class AstronomicalBodyHelper { public static final int DAYS_PER_LUNAR_MONTH = 8; /** Ticks in one day — the platform's rate, NOT a planet's rotational period (that is per-dim). */ public static final int TICKS_PER_DAY = 24000; + /** + * Ticks in one year — the two above composed, so a rate stated per year has ONE conversion into + * the clock the game actually counts. A galactic rotation is quoted per year and evaluated per + * tick, and writing that product at the call site is how the two calendars drift apart. + */ + public static final int TICKS_PER_YEAR = DAYS_PER_YEAR * TICKS_PER_DAY; /** * Returns the size multiplier for a body at the input distance, relative to either 1AU or the moon's orbital distance, depending on parent body diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index 88899f8c6..709bd82ad 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -84,8 +84,8 @@ public class XMLPlanetLoader { private static final String ATTR_TIDALLY_LOCKABLE = "tidallyLockable"; private static final String ATTR_DENSITY = "density"; private static final String ATTR_MINSPACING = "minSpacing"; - private static final String ATTR_CLUSTERSCALE = "clusterScale"; - private static final String ATTR_VOIDFRACTION = "voidFraction"; + private static final String ATTR_GALAXYSPACING = "galaxySpacing"; + private static final String ATTR_GALAXYDENSITY = "galaxyDensity"; private static final String ATTR_MINSIZE = "minSize"; private static final String ATTR_MAXSIZE = "maxSize"; private static final String ELEMENT_PLANET = "planet"; @@ -223,6 +223,19 @@ private static int attrInt(Node node, String name, int def) { } } + private static long attrLong(Node node, String name, long def) { + String v = attr(node, name); + if (v == null || v.trim().isEmpty()) { + return def; + } + try { + return Long.parseLong(v.trim()); + } catch (NumberFormatException e) { + AdvancedRocketry.logger.warn("Invalid " + name + " in : " + v); + return def; + } + } + private static double attrDouble(Node node, String name, double def) { String v = attr(node, name); if (v == null || v.trim().isEmpty()) { @@ -241,8 +254,8 @@ private GalaxyGenConfig readGalaxyGen(Node node) { GalaxyGenConfig defaults = GalaxyGenConfig.defaults(); double density = attrDouble(node, ATTR_DENSITY, defaults.density); int minSpacing = attrInt(node, ATTR_MINSPACING, defaults.minSpacing); - int clusterScale = attrInt(node, ATTR_CLUSTERSCALE, defaults.clusterScale); - double voidFraction = attrDouble(node, ATTR_VOIDFRACTION, defaults.voidFraction); + long galaxySpacing = attrLong(node, ATTR_GALAXYSPACING, defaults.galaxySpacing); + double galaxyDensity = attrDouble(node, ATTR_GALAXYDENSITY, defaults.galaxyDensity); List types = new ArrayList<>(); NodeList children = node.getChildNodes(); @@ -257,7 +270,9 @@ private GalaxyGenConfig readGalaxyGen(Node node) { } } // An empty list falls back to the default archetypes (handled by the config ctor). - return new GalaxyGenConfig(density, minSpacing, clusterScale, voidFraction, types); + // The GALAXY archetype table is not authorable yet: it ships as a stock table in code, and a + // element is the natural place to override it when a pack needs to. + return new GalaxyGenConfig(minSpacing, density, galaxySpacing, galaxyDensity, types, null); } /** @@ -466,8 +481,8 @@ private static Element writeGalaxyGen(Document doc, GalaxyGenConfig cfg) { Element e = doc.createElement(ELEMENT_GALAXYGEN); e.setAttribute(ATTR_DENSITY, Double.toString(cfg.density)); e.setAttribute(ATTR_MINSPACING, Integer.toString(cfg.minSpacing)); - e.setAttribute(ATTR_CLUSTERSCALE, Integer.toString(cfg.clusterScale)); - e.setAttribute(ATTR_VOIDFRACTION, Double.toString(cfg.voidFraction)); + e.setAttribute(ATTR_GALAXYSPACING, Long.toString(cfg.galaxySpacing)); + e.setAttribute(ATTR_GALAXYDENSITY, Double.toString(cfg.galaxyDensity)); for (GalaxyGenConfig.StarType t : cfg.starTypes) { Element st = doc.createElement(ELEMENT_STARTYPE); st.setAttribute(ATTR_TEMP, Integer.toString(t.temperature)); diff --git a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java index b85ea7034..302d3ac35 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java @@ -150,7 +150,9 @@ public void oneOrbitalDistanceMeansOneDistanceInBothFamilies() { / authored.orbitalDistance(); ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator( - new GalaxyGenConfig(1.0d, GalaxyGenConfig.DEFAULT_MIN_SPACING, 8, 0.0d, null)); + new GalaxyGenConfig(GalaxyGenConfig.DEFAULT_MIN_SPACING, 1.0d, + GalaxyGenConfig.DEFAULT_GALAXY_SPACING, GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, + null, null)); long spacing = GalaxyGenConfig.DEFAULT_MIN_SPACING; Optional seat = gen.anchorAt(0xBEEFL, GalacticCoord.ofSectorLocal(spacing, spacing, spacing, 0L, 0L, 0L)); diff --git a/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java b/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java index 97cea52f5..bfa15d152 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java @@ -514,15 +514,18 @@ public void oreGenPropertiesSurviveWriteReadRoundTrip() throws Exception { @Test public void galaxyGenElementParsesIntoConfig() throws IOException { DimensionPropertyCoupling c = parse(galaxy( - "\n" + "\n" + " \n" + " \n" + "\n")); assertNotNull("a element must parse into a config", c.galaxyGenConfig); assertEquals(0.42d, c.galaxyGenConfig.density, 1e-9); assertEquals(7, c.galaxyGenConfig.minSpacing); - assertEquals(9, c.galaxyGenConfig.clusterScale); - assertEquals(0.3d, c.galaxyGenConfig.voidFraction, 1e-9); + assertEquals(900000L, c.galaxyGenConfig.galaxySpacing); + assertEquals(0.3d, c.galaxyGenConfig.galaxyDensity, 1e-9); + assertFalse("the stock galaxy archetypes stand in when XML declares none", + c.galaxyGenConfig.galaxyTypes.isEmpty()); assertEquals(2, c.galaxyGenConfig.starTypes.size()); assertEquals(55, c.galaxyGenConfig.starTypes.get(0).temperature); assertEquals(3, c.galaxyGenConfig.starTypes.get(0).weight); @@ -537,7 +540,8 @@ public void absentGalaxyGenLeavesConfigNull() throws IOException { @Test public void galaxyGenRoundTripsThroughWriteXml() throws IOException { GalaxyGenConfig parsed = parse(galaxy( - "\n" + "\n" + " \n" + "\n")).galaxyGenConfig; @@ -554,8 +558,8 @@ public void galaxyGenRoundTripsThroughWriteXml() throws IOException { assertNotNull("the written galaxy must round-trip its ", round); assertEquals(0.25d, round.density, 1e-9); assertEquals(5, round.minSpacing); - assertEquals(12, round.clusterScale); - assertEquals(0.45d, round.voidFraction, 1e-9); + assertEquals(1200000L, round.galaxySpacing); + assertEquals(0.45d, round.galaxyDensity, 1e-9); assertEquals(60, round.starTypes.get(0).temperature); assertEquals(9, round.starTypes.get(0).weight); } finally { diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/ProceduralPlanetRealizationE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/ProceduralPlanetRealizationE2ETest.java index a97ab29ce..cee7804a3 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/ProceduralPlanetRealizationE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/ProceduralPlanetRealizationE2ETest.java @@ -39,12 +39,17 @@ public class ProceduralPlanetRealizationE2ETest extends AbstractHeadlessServerTest { /** - * A tiny super-cell spacing. A system's anchor is seated in the middle band of its super-cell, so at - * the production spacing of 512 the nearest system is hundreds of cells away and a bounded probe - * sweep finds an empty universe. The spacing is a balance knob, and nothing here asserts one. + * A compact star spacing, and it has a floor: a system's bodies stand where their own orbits put + * them, so a super-cell has to be wide enough to hold one. Below roughly 170 000 cells a system + * starts losing its outer worlds and below a few cells only the star survives — which is a correct + * outcome of "a system that will not fit loses BODIES, never scale", and a fixture with no landable + * body in it. What is compact here is the distance BETWEEN stars, so a bounded sweep finds several. + * + *

The spacing is a balance knob and nothing here asserts one.

*/ - private static final String GEN_INSTALL = "artest space gen-install 0.9 4 8 0.0 987654321"; - private static final int SWEEP_RADIUS = 8; + private static final String GEN_INSTALL = "artest space gen-install 0.9 2000000 987654321"; + /** In SUPER-CELLS: the probe sweeps the partition the generator itself walks. */ + private static final int SWEEP_RADIUS = 4; private String exec(String cmd) throws Exception { return String.join("\n", client().execute(cmd)); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index 063bdf5f4..eb5056017 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -12,6 +12,7 @@ import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Galaxy; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.StarSystem; import zmaster587.advancedRocketry.universe.SystemBody; @@ -27,10 +28,15 @@ * Contract tests for the deterministic clustered galaxy generator. Pure-JUnit; no MC bootstrap. * *

Pins the generation CONTRACTS: pure determinism over {@code (seed, cell)}, the minimum-spacing - * guarantee, the separation floor between two seats, that the distribution actually clusters (void + - * dense regions), that {@code systemsInRegion} agrees with {@code systemAt}, and that the tunable - * params drive the outcome. Balance numbers are exercised as inputs, never pinned as expected - * values.

+ * guarantee, the separation floor between two seats, that the star field is its GALAXY's density + * profile (it thins outwards and stops at the declared radius), that {@code systemsInRegion} agrees + * with {@code systemAt}, and that the tunable params drive the outcome. Balance numbers are exercised + * as inputs, never pinned as expected values.

+ * + *

Every sweep here sits near the ORIGIN, which is the home galaxy's centre — the one place + * guaranteed to be inside a galaxy under every seed. A sweep elsewhere would be sampling whatever the + * seed happened to put there, which is a different claim. The galaxy lattice itself is + * {@code GalaxyFieldTest}'s subject.

* *

Sampling is by SUPER-CELL, never by cell. A star seat is one cell in a cube of tens of * millions, so sweeping cells finds nothing whatever the galaxy holds — and a spacing small enough to @@ -48,12 +54,18 @@ private static GalacticCoord cell(long sx, long sy, long sz) { /** The shipped spacing: what the sampled galaxy is is what the game ships. */ private static final int SPACING = GalaxyGenConfig.DEFAULT_MIN_SPACING; - private static GalaxyGenConfig cfg(double density, int spacing, int clusterScale, double voidFraction) { - return new GalaxyGenConfig(density, spacing, clusterScale, voidFraction, null); + /** + * A config at the shipped galaxy lattice, varying only how full a galaxy's densest point is. Every + * sweep in this class sits near the ORIGIN, which is the home galaxy's centre, so {@code density} + * is the whole of what decides whether the sampled sky has stars in it. + */ + private static GalaxyGenConfig cfg(double density, int spacing) { + return new GalaxyGenConfig(spacing, density, GalaxyGenConfig.DEFAULT_GALAXY_SPACING, + GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, null, null); } private static GalaxyGenConfig defaultsCfg() { - return cfg(0.35d, SPACING, 16, 0.6d); + return cfg(0.35d, SPACING); } /** Iterate an inclusive box of SUPER-CELLS, calling the visitor with each one's probe cell. */ @@ -115,7 +127,7 @@ public void differentSeedsProduceDifferentGalaxies() { @Test public void minimumSpacingIsRespected() { // At most one system per minSpacing-cube super-cell, anywhere in the sampled volume. - GalaxyGenConfig config = cfg(0.9d, SPACING, 8, 0.0d); // dense, no void: stress spacing + GalaxyGenConfig config = cfg(0.9d, SPACING); // dense, no void: stress spacing ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); Map perSuperCell = new HashMap<>(); for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 4)) { @@ -136,7 +148,7 @@ public void noTwoStarsStandCloserThanTheSeparationFloor() { // The floor is what makes a near-pair of seats impossible, and it is what stops two unrelated // systems — two names, two frames, no gravitational relation — from being read as a binary. // Multiplicity is something a system states about itself, never something the lattice fakes. - GalaxyGenConfig config = cfg(1.0d, SPACING, 8, 0.0d); // every cube occupied: the tightest case + GalaxyGenConfig config = cfg(1.0d, SPACING); // every cube occupied: the tightest case ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); List seats = anchors(gen, SEED, SPACING, 2); assertTrue("the sweep must find systems", seats.size() > 10); @@ -156,7 +168,7 @@ public void aSeatIsNotConfinedToTheMiddleOfItsCube() { // The seat used to be pinned into the middle quarter per axis — 1.6 % of the cube's volume — // which reads as a lattice of tight clumps with guaranteed-empty walls. What replaces it is a // margin sized by what a system NEEDS, so most of the cube is reachable. - GalaxyGenConfig config = cfg(1.0d, SPACING, 8, 0.0d); + GalaxyGenConfig config = cfg(1.0d, SPACING); ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); long s = config.minSpacing; double nearestFaceFraction = 1d; @@ -173,34 +185,23 @@ public void aSeatIsNotConfinedToTheMiddleOfItsCube() { } @Test - public void distributionClustersIntoGalaxiesAndVoid() { - // A strongly-clustered config: expect BOTH occupied sub-regions and entirely-empty (void) ones. - GalaxyGenConfig config = cfg(0.6d, SPACING, 8, 0.6d); - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); - - int emptyBlocks = 0; - int nonEmptyBlocks = 0; - // Scan 16x16 coarse blocks (each 6x6x1 super-cells) across a wide plane. - for (long bx = -8; bx < 8; bx++) { - for (long by = -8; by < 8; by++) { - boolean any = false; - for (long dx = 0; dx < 6 && !any; dx++) { - for (long dy = 0; dy < 6 && !any; dy++) { - if (gen.anchorAt(SEED, cell((bx * 6 + dx) * SPACING, (by * 6 + dy) * SPACING, 0)) - .isPresent()) { - any = true; - } - } - } - if (any) { - nonEmptyBlocks++; - } else { - emptyBlocks++; - } - } - } - assertTrue("clustering must leave genuinely empty void regions", emptyBlocks > 0); - assertTrue("clustering must leave genuinely populated regions", nonEmptyBlocks > 0); + public void starsStopAtTheirGalaxysDeclaredEdge() { + // The star field is the GALAXY's density profile, so where a galaxy ends the stars end. This + // is what an independent per-cell mask could not do: drawn above the percolation threshold it + // produced one unbounded sponge, with no edge to reach and no answer to "which galaxy is this". + // + // Sampled against the home galaxy's OWN radius rather than a hard-coded distance: the radius + // is drawn per seed, so a fixed number would be testing one draw. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(1.0d, SPACING)); + Galaxy home = gen.galaxies().home(SEED); + + int inside = seatsInBlockAround(gen, 0L, 3); + long beyondEdge = UniverseScale.cellsForLightYears(home.radiusLy() * 1.5d); + int outside = seatsInBlockAround(gen, beyondEdge, 3); + + assertTrue("the galaxy's core must hold stars (found " + inside + ")", inside > 0); + assertEquals("past the declared radius of " + (long) home.radiusLy() + + " ly there must be nothing", 0, outside); } @Test @@ -244,20 +245,23 @@ public void systemsInRegionHandlesSwappedBounds() { } @Test - public void voidFractionDrivesOccupancy() { - int allVoid = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.8d, SPACING, 8, 1.0d)), - SEED, SPACING, 6).size(); - int noVoid = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.8d, SPACING, 8, 0.0d)), - SEED, SPACING, 6).size(); - assertEquals("voidFraction=1 must yield an empty galaxy", 0, allVoid); - assertTrue("voidFraction=0 must populate the galaxy", noVoid > 0); + public void aGalaxysProfileThinsTheStarFieldOutwards() { + // The profile is not a mask with two states. A galaxy is densest at its nucleus and thins with + // radius, so the same density knob has to place more stars near the centre than out at the rim + // — that gradient is the whole difference between a galaxy and a uniform fog. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(1.0d, SPACING)); + Galaxy home = gen.galaxies().home(SEED); + + int core = seatsInBlockAround(gen, 0L, 4); + int rim = seatsInBlockAround(gen, UniverseScale.cellsForLightYears(home.radiusLy() * 0.8d), 4); + assertTrue("the core must be denser than the rim (" + core + " vs " + rim + ")", core > rim); } @Test public void densityDrivesOccupancy() { - int sparse = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.1d, SPACING, 8, 0.0d)), + int sparse = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.1d, SPACING)), SEED, SPACING, 7).size(); - int dense = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.9d, SPACING, 8, 0.0d)), + int dense = occupiedSeats(new ClusteredGalaxyGenerator(cfg(0.9d, SPACING)), SEED, SPACING, 7).size(); assertTrue("higher density must place more systems (" + sparse + " vs " + dense + ")", dense > sparse); @@ -269,7 +273,9 @@ public void starTypesAreDrawnFromTheConfiguredSetAndWeighted() { List types = new ArrayList<>(); types.add(new GalaxyGenConfig.StarType(50, 0.5f, 1.0f, 100)); // common types.add(new GalaxyGenConfig.StarType(250, 2.0f, 3.0f, 1)); // rare - GalaxyGenConfig config = new GalaxyGenConfig(0.9d, SPACING, 8, 0.0d, types); + GalaxyGenConfig config = new GalaxyGenConfig(SPACING, 0.9d, + GalaxyGenConfig.DEFAULT_GALAXY_SPACING, GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, + types, null); ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); int common = 0; @@ -311,7 +317,7 @@ public void starTypesAreDrawnFromTheConfiguredSetAndWeighted() { @Test public void proceduralSystemIdsAreNegative() { - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(0.9d, SPACING, 8, 0.0d)); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(0.9d, SPACING)); boolean sawAny = false; for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 2)) { sawAny = true; @@ -323,20 +329,21 @@ public void proceduralSystemIdsAreNegative() { @Test public void configClampsAndDefaults() { - GalaxyGenConfig c = new GalaxyGenConfig(5.0d, -3, 0, -1.0d, null); + GalaxyGenConfig c = new GalaxyGenConfig(-3, 5.0d, -7L, -1.0d, null, null); assertEquals("density clamps to [0,1]", 1.0d, c.density, 0d); - assertEquals("voidFraction clamps to [0,1]", 0.0d, c.voidFraction, 0d); + assertEquals("galaxyDensity clamps to [0,1]", 0.0d, c.galaxyDensity, 0d); assertTrue("minSpacing floors at 1", c.minSpacing >= 1); - assertTrue("clusterScale floors at 1", c.clusterScale >= 1); + assertTrue("galaxySpacing floors at 1", c.galaxySpacing >= 1L); assertFalse("empty star types fall back to defaults", c.starTypes.isEmpty()); + assertFalse("empty galaxy types fall back to defaults", c.galaxyTypes.isEmpty()); } @Test public void configClampsNaNToZero() { - // A NaN attribute (Double.parseDouble accepts "NaN") must not poison the density/void gates. - GalaxyGenConfig c = new GalaxyGenConfig(Double.NaN, 1, 1, Double.NaN, null); + // A NaN attribute (Double.parseDouble accepts "NaN") must not poison either occupancy gate. + GalaxyGenConfig c = new GalaxyGenConfig(1, Double.NaN, 1L, Double.NaN, null, null); assertEquals("NaN density clamps to 0", 0.0d, c.density, 0d); - assertEquals("NaN voidFraction clamps to 0", 0.0d, c.voidFraction, 0d); + assertEquals("NaN galaxyDensity clamps to 0", 0.0d, c.galaxyDensity, 0d); } @Test @@ -346,7 +353,8 @@ public void hugeStarWeightsDoNotCollapseTheDistribution() { types.add(new GalaxyGenConfig.StarType(50, 0.5f, 1.0f, Integer.MAX_VALUE)); types.add(new GalaxyGenConfig.StarType(250, 2.0f, 3.0f, Integer.MAX_VALUE)); ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.9d, SPACING, 8, 0.0d, types)); + new GalaxyGenConfig(SPACING, 0.9d, GalaxyGenConfig.DEFAULT_GALAXY_SPACING, + GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, types, null)); Set seenTemps = new HashSet<>(); for (long x = -20; x <= 20; x++) { @@ -366,7 +374,7 @@ public void hugeStarWeightsDoNotCollapseTheDistribution() { public void proceduralBodiesGetTheirOwnCellsInsideTheSuperCell() { // A system is an anchored NEIGHBOURHOOD — the star holds the anchor cell, each planet/belt its // own cell (snapped to that cell's centre), all inside the anchor's minSpacing super-cell. - GalaxyGenConfig config = cfg(0.9d, SPACING, 8, 0.0d); + GalaxyGenConfig config = cfg(0.9d, SPACING); ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); long s = config.minSpacing; boolean checkedAny = false; @@ -419,7 +427,7 @@ public void aBodyStandsExactlyWhereItsOrbitalDistanceSaysItDoes() { // distance d is d units from its star, in blocks, and its cell NAME is a reading of that same // position rather than a second layout arithmetic beside it. When those two came apart, the // science said one thing and the flight time said another. - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(0.9d, SPACING, 8, 0.0d)); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(0.9d, SPACING)); int checked = 0; for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 1)) { List bodies = gen.bodiesFor(SEED, anchor); @@ -450,7 +458,7 @@ public void aBodyStandsExactlyWhereItsOrbitalDistanceSaysItDoes() { public void aSystemNeverReachesPastItsOwnClearSpace() { // The bound that replaces "a system is a fraction of the distance to the next star": named // bodies stay inside half the separation floor, whatever a star's own zone would have drawn. - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(1.0d, SPACING, 8, 0.0d)); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(1.0d, SPACING)); int checked = 0; for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 1)) { for (SystemBody body : gen.bodiesFor(SEED, anchor)) { @@ -469,7 +477,7 @@ public void tinySpacingDegeneratesIntoALoneStar() { // would have to share that cell, which at most one real body per cell forbids — so the system // degenerates to its star alone. Degenerate but CONSISTENT: attribution stays exact, nothing // escapes the box, and no cell ends up with two destinations in it. - ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(0.9d, 1, 8, 0.0d)); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(0.9d, 1)); boolean checkedAny = false; for (long x = -6; x <= 6; x++) { GalacticCoord c = cell(x, 0, 0); @@ -488,7 +496,7 @@ public void tinySpacingDegeneratesIntoALoneStar() { @Test public void anchorAtAttributesEveryCellOfAnOccupiedSuperCell() { - GalaxyGenConfig config = cfg(0.9d, SPACING, 8, 0.0d); + GalaxyGenConfig config = cfg(0.9d, SPACING); ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); long s = config.minSpacing; boolean checkedAny = false; @@ -528,6 +536,28 @@ private static List anchors(ClusteredGalaxyGenerator gen, long se return out; } + /** + * How many seats a {@code (2r+1)³} block of super-cells holds, centred {@code offsetCells} out + * along +X from the origin — the origin being the home galaxy's centre. Sampling a BLOCK rather + * than a single super-cell is what makes the count a reading of the density there instead of one + * coin toss. + */ + private static int seatsInBlockAround(ClusteredGalaxyGenerator gen, long offsetCells, long r) { + Set seen = new HashSet<>(); + for (long x = -r; x <= r; x++) { + for (long y = -r; y <= r; y++) { + for (long z = -r; z <= r; z++) { + Optional a = gen.anchorAt(SEED, + cell(offsetCells + x * SPACING, y * SPACING, z * SPACING)); + if (a.isPresent()) { + seen.add(a.get().cellKey()); + } + } + } + } + return seen.size(); + } + private static Set occupiedSeats(ClusteredGalaxyGenerator gen, long seed, long spacing, long r) { Set keys = new HashSet<>(); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java new file mode 100644 index 000000000..8300cb4e1 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java @@ -0,0 +1,330 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Galaxy; +import zmaster587.advancedRocketry.universe.GalaxyField; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.UniverseScale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for the galaxy lattice — the tier above the star lattice. + * + *

What is pinned: a galaxy is a pure function of {@code (seed, galaxy cell)}; the galaxy index is + * DERIVED from the sector and nothing is stored; a galaxy never straddles its own cell face, which is + * what makes "which galaxy is this point in" an O(1) question with one answer; radius is drawn + * CONDITIONAL ON TYPE; the home galaxy exists under every seed while still differing between them; + * and the cosmic-web hook is neutral today, so galaxy density comes out uniform.

+ */ +public class GalaxyFieldTest { + + private static GalaxyGenConfig cfg(double galaxyDensity) { + return new GalaxyGenConfig(GalaxyGenConfig.DEFAULT_MIN_SPACING, 0.9d, + GalaxyGenConfig.DEFAULT_GALAXY_SPACING, galaxyDensity, null, null); + } + + private static GalaxyField field(double galaxyDensity) { + return new GalaxyField(cfg(galaxyDensity)); + } + + @Test + public void theHomeGalaxyExistsUnderEverySeed() { + // Authored content is placed at absolute coordinates near the origin, and a galaxy is otherwise + // a hash draw that may simply not be there. Without the reserved cell the shipped solar system + // would land in intergalactic space on almost every seed. + GalaxyField f = field(GalaxyGenConfig.DEFAULT_GALAXY_DENSITY); + for (long seed = 1L; seed <= 200L; seed++) { + Galaxy home = f.home(seed); + assertNotNull("seed " + seed + " has no home galaxy", home); + assertEquals("the home galaxy is centred on the origin", GalacticCoord.ORIGIN.cellKey(), + home.centre().cellKey()); + assertTrue("seed " + seed + "'s home galaxy is only " + home.radiusLy() + + " ly across, under the guaranteed minimum", + home.radiusLy() >= UniverseScale.MIN_HOME_GALAXY_RADIUS_LY); + } + } + + @Test + public void theHomeGalaxyIsSeatedEvenWhenNoOtherGalaxyIs() { + // Its EXISTENCE is reserved, not its probability: a config that places no galaxies at all + // still has to have the one the player lives in. + GalaxyField f = field(0d); + assertNotNull(f.home(7L)); + int others = 0; + for (long gx = -3L; gx <= 3L; gx++) { + for (long gy = -3L; gy <= 3L; gy++) { + if (f.galaxyAtIndex(7L, gx, gy, 0L).isPresent() && !GalaxyField.isHomeCell(gx, gy, 0L)) { + others++; + } + } + } + assertEquals("galaxyDensity=0 must leave everything but the home cell void", 0, others); + } + + @Test + public void theHomeGalaxyStillDiffersBetweenSeeds() { + // Only its existence and its centre are fixed. If its type and size were fixed too, every + // world would open on the same sky. + GalaxyField f = field(GalaxyGenConfig.DEFAULT_GALAXY_DENSITY); + Set shapes = new HashSet<>(); + for (long seed = 1L; seed <= 50L; seed++) { + Galaxy home = f.home(seed); + shapes.add(home.type().name + "@" + (long) home.radiusLy()); + } + assertTrue("every seed produced the same home galaxy", shapes.size() > 1); + } + + @Test + public void aGalaxyIsAPureFunctionOfSeedAndCell() { + GalaxyField f = field(GalaxyGenConfig.DEFAULT_GALAXY_DENSITY); + for (long gx = -4L; gx <= 4L; gx++) { + Optional a = f.galaxyAtIndex(99L, gx, 1L, -2L); + Optional b = f.galaxyAtIndex(99L, gx, 1L, -2L); + assertEquals("presence must be stable", a.isPresent(), b.isPresent()); + if (a.isPresent()) { + assertEquals(a.get().toString(), b.get().toString()); + } + } + } + + @Test + public void theGalaxyIndexIsDerivedFromTheSector() { + // No stored tier, no new coordinate field: a coarse reading of the sector space that already + // exists. Every sector of one galaxy cell must name the same galaxy. + GalaxyGenConfig config = cfg(1.0d); + GalaxyField f = new GalaxyField(config); + long s = config.galaxySpacing; + // The lattice is offset by half a cell, so the ORIGIN is a cell CENTRE. Without that, every + // sector with a negative coordinate would sit in a neighbouring cell and the space around the + // shipped solar system would be reading someone else's galaxy. + assertEquals(0L, GalaxyField.galaxyIndex(0L, s)); + assertEquals("just below the origin is still the home cell", 0L, + GalaxyField.galaxyIndex(-1L, s)); + assertEquals(0L, GalaxyField.galaxyIndex(-s / 2L, s)); + assertEquals(0L, GalaxyField.galaxyIndex(s / 2L - 1L, s)); + assertEquals("half a cell out is the next one", 1L, GalaxyField.galaxyIndex(s / 2L + 1L, s)); + assertEquals(-1L, GalaxyField.galaxyIndex(-s / 2L - 1L, s)); + assertEquals("the home cell's low corner is half a cell below the origin", -(s / 2L), + GalaxyField.cellLowCorner(0L, s)); + + Galaxy home = f.home(5L); + for (long probe : new long[] {-s / 2L, -1L, 0L, 1L, s / 3L, s / 2L - 1L}) { + Optional owning = f.galaxyOwningSector(5L, probe, 0L, 0L); + assertTrue("sector " + probe + " must belong to a galaxy cell that has one", + owning.isPresent()); + assertEquals("and it must be the same galaxy throughout the cell", home.toString(), + owning.get().toString()); + } + } + + @Test + public void everyPointIsInAGalaxyCellButNotEveryPointIsInAGalaxy() { + // The two questions are different and both have to be answerable: a galaxy occupies a small + // sphere inside its cell, and the rest of that cell is void. There is no "nowhere" state. + GalaxyField f = field(1.0d); + Galaxy home = f.home(11L); + long inside = UniverseScale.cellsForLightYears(home.radiusLy() * 0.5d); + long outside = UniverseScale.cellsForLightYears(home.radiusLy() * 4d); + + assertTrue(f.galaxyOwningSector(11L, inside, 0L, 0L).isPresent()); + assertTrue("a point at half the radius is in the galaxy", home.containsSector(inside, 0L, 0L)); + + Optional farOwner = f.galaxyOwningSector(11L, outside, 0L, 0L); + assertTrue("a point deep in the same cell still HAS an owning cell", farOwner.isPresent()); + assertEquals(home.toString(), farOwner.get().toString()); + assertFalse("but it is not inside the galaxy", home.containsSector(outside, 0L, 0L)); + assertEquals("so the profile there is zero", 0d, home.densityAtSector(outside, 0L, 0L), 0d); + } + + @Test + public void aGalaxyNeverStraddlesItsOwnCellFace() { + // Containment is what keeps three things true at once: at most one galaxy per cell, galaxies + // that cannot overlap, and an ownership answer that reads the containing cell and nothing else. + GalaxyGenConfig config = cfg(1.0d); + GalaxyField f = new GalaxyField(config); + long s = config.galaxySpacing; + int checked = 0; + for (long gx = -3L; gx <= 3L; gx++) { + for (long gy = -2L; gy <= 2L; gy++) { + for (long gz = -2L; gz <= 2L; gz++) { + Optional g = f.galaxyAtIndex(4242L, gx, gy, gz); + if (!g.isPresent() || GalaxyField.isHomeCell(gx, gy, gz)) { + continue; + } + long reach = UniverseScale.cellsForLightYears(g.get().radiusLy()); + assertInsideCell("x", g.get().centre().sectorX(), gx, s, reach); + assertInsideCell("y", g.get().centre().sectorY(), gy, s, reach); + assertInsideCell("z", g.get().centre().sectorZ(), gz, s, reach); + checked++; + } + } + } + assertTrue("the sweep must find galaxies", checked > 10); + } + + private static void assertInsideCell(String axis, long centre, long index, long spacing, + long reach) { + long lo = GalaxyField.cellLowCorner(index, spacing); + long hi = lo + spacing - 1L; + assertTrue("a galaxy reaches past its cell's low " + axis + " face", centre - reach >= lo); + assertTrue("a galaxy reaches past its cell's high " + axis + " face", centre + reach <= hi); + } + + @Test + public void radiusIsDrawnConditionalOnItsType() { + // Never independently. Independent draws produce dwarfs the size of a spiral and spirals the + // size of a dwarf — a real galaxy's type and its size are one fact, not two. + GalaxyField f = field(1.0d); + int checked = 0; + for (long gx = -6L; gx <= 6L; gx++) { + for (long gy = -3L; gy <= 3L; gy++) { + Optional g = f.galaxyAtIndex(31337L, gx, gy, 0L); + if (!g.isPresent()) { + continue; + } + GalaxyGenConfig.GalaxyType t = g.get().type(); + assertTrue(g.get() + " falls outside its own type's band", + g.get().radiusLy() >= t.minRadiusLy && g.get().radiusLy() <= t.maxRadiusLy); + assertTrue("a type with no arms must not carry a spiral's structure", + t.armCount >= 0); + checked++; + } + } + assertTrue(checked > 10); + } + + @Test + public void galaxyDensityDrivesHowManyGalaxiesThereAre() { + int sparse = countGalaxies(field(0.1d), 5L); + int dense = countGalaxies(field(0.9d), 5L); + assertTrue("a higher galaxyDensity must seat more galaxies (" + sparse + " vs " + dense + ")", + dense > sparse); + } + + @Test + public void galaxyDensityIsUniformWhileTheCosmicWebIsANeutralConstant() { + // The web slot exists and is deliberately the constant 1 today: galaxy density is not REQUIRED + // to be uniform, and this is where non-uniformity will live. Until it does, the occupied + // fraction must come out AT the configured density rather than biased by a half-built field. + GalaxyField f = field(0.5d); + int occupied = 0; + int total = 0; + for (long gx = -8L; gx <= 8L; gx++) { + for (long gy = -8L; gy <= 8L; gy++) { + for (long gz = -3L; gz <= 3L; gz++) { + if (GalaxyField.isHomeCell(gx, gy, gz)) { + continue; // reserved, so it is not a sample of the draw + } + total++; + if (f.galaxyAtIndex(6060L, gx, gy, gz).isPresent()) { + occupied++; + } + } + } + } + double fraction = occupied / (double) total; + assertEquals("the occupied fraction must sit at galaxyDensity", 0.5d, fraction, 0.05d); + } + + @Test + public void theVoidBetweenGalaxiesHoldsNoSystems() { + // The generator's own view of the same fact: outside every galaxy the profile is zero, so the + // intergalactic void is what the profile leaves empty rather than a second rule someone has to + // remember to apply. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(1.0d)); + Galaxy home = gen.galaxies().home(77L); + long beyond = UniverseScale.cellsForLightYears(home.radiusLy() * 3d); + long spacing = GalaxyGenConfig.DEFAULT_MIN_SPACING; + for (long i = 0; i < 40; i++) { + GalacticCoord probe = GalacticCoord.ofSectorLocal(beyond + i * spacing, 0L, 0L, 0L, 0L, 0L); + assertFalse("a system turned up in intergalactic space at " + probe.cellKey(), + gen.anchorAt(77L, probe).isPresent()); + } + } + + @Test + public void aGalaxyCellFitsInsideOneLongOfBlocks() { + // This is what the galaxy SIZE was chosen for, and it is a structural claim rather than a + // balance one. Out in the void a position is an offset from its galaxy cell's origin, so that + // offset has to span a whole cell; a Milky-Way-sized galaxy at a realistic separation would + // put the cell past the long range and force the void into a second, coarser representation. + // Choosing this scale buys one primitive instead of two. + long spacing = GalaxyGenConfig.DEFAULT_GALAXY_SPACING; + long limitCells = Long.MAX_VALUE / GalacticCoord.CELL; + assertTrue("a galaxy cell of " + spacing + " cells overflows a long of blocks", + spacing <= limitCells); + // The bound is not the edge but the DIAGONAL: two points in one void cell can be that far + // apart, and a separation that cannot be expressed is a separation that silently wraps. + assertTrue("a galaxy cell's diagonal overflows a long of blocks — the margin is only " + + String.format("%.2f", limitCells / (double) spacing) + "x on the edge", + Math.sqrt(3d) * spacing <= limitCells); + } + + @Test + public void aGalaxyHoldsAPopulationOfTheRightOrder() { + // Estimated rather than counted: sweeping every super-cell of a galaxy is 10^8 draws. The + // profile is integrated by Monte Carlo over the galaxy's own sphere, which is the same + // function the generator consults, so this measures the shipped shape and not a model of it. + // + // The band is deliberately wide — three orders. What it guards is the ORDER: a galaxy holding + // thousands would make interstellar travel a tour of a village, and one holding billions would + // put the cell past the long range the test above depends on. + GalaxyGenConfig config = cfg(GalaxyGenConfig.DEFAULT_GALAXY_DENSITY); + GalaxyField f = new GalaxyField(config); + Galaxy home = f.home(0xC0FFEEL); + double superCellLy = UniverseScale.lightYearsForCells(config.minSpacing); + double sphereLy3 = 4d / 3d * Math.PI * Math.pow(home.radiusLy(), 3); + double superCells = sphereLy3 / Math.pow(superCellLy, 3); + + // A fixed LCG, so the estimate is the same number on every run and a red is a real change. + long state = 0x2545F4914F6CDD1DL; + int samples = 200_000; + double sum = 0d; + for (int i = 0; i < samples; i++) { + double[] p = new double[3]; + for (int axis = 0; axis < 3; axis++) { + state = state * 6364136223846793005L + 1442695040888963407L; + p[axis] = ((state >>> 11) * 0x1.0p-53 - 0.5d) * 2d * home.radiusLy(); + } + sum += home.densityAt(p[0], p[1], p[2]); + } + // The samples fill the CUBE around the galaxy; the sphere is pi/6 of it, and densityAt is + // already zero outside the radius, so the cube mean scales straight onto the cube's volume. + double cubeLy3 = Math.pow(2d * home.radiusLy(), 3); + double meanOverSphere = (sum / samples) * cubeLy3 / sphereLy3; + double systems = config.density * superCells * meanOverSphere; + + System.out.println("home galaxy " + home + ": ~" + (long) systems + " systems (" + + (long) superCells + " super-cells in its sphere, mean profile " + + String.format("%.5f", meanOverSphere) + ")"); + assertTrue("a galaxy holding only " + (long) systems + " systems is a village", + systems > 1e4d); + assertTrue("a galaxy holding " + (long) systems + " systems is past the scale this " + + "lattice was sized for", systems < 1e7d); + } + + private static int countGalaxies(GalaxyField f, long seed) { + int found = 0; + for (long gx = -5L; gx <= 5L; gx++) { + for (long gy = -5L; gy <= 5L; gy++) { + for (long gz = -2L; gz <= 2L; gz++) { + if (!GalaxyField.isHomeCell(gx, gy, gz) && f.galaxyAtIndex(seed, gx, gy, gz).isPresent()) { + found++; + } + } + } + } + return found; + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java new file mode 100644 index 000000000..483bd2606 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java @@ -0,0 +1,202 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.Galaxy; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.UniverseScale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for one galaxy as a shape: what it contains, how its density falls off, and how it + * turns. Pure-JUnit; no MC bootstrap, no generator. + * + *

What is pinned is the SHAPE of each law, never a tuned number: that the boundary is the declared + * radius and not a level of the profile, that density falls with radius and with height above the + * plane, that a disc really is flatter than it is wide, that the arms modulate rather than gate, and + * that rotation shears differently for a dwarf than for a massive spiral. The constants those laws + * carry are balance knobs and are fed in as inputs.

+ */ +public class GalaxyTest { + + private static final double RADIUS = 1500d; + + private static GalaxyGenConfig.GalaxyType spiral() { + return new GalaxyGenConfig.GalaxyType("Spiral", GalaxyGenConfig.GalaxyProfile.DISC, + 900d, 2200d, 0.02d, 2, 220d, 0.08d, 7); + } + + private static GalaxyGenConfig.GalaxyType smoothDisc() { + return new GalaxyGenConfig.GalaxyType("Smooth", GalaxyGenConfig.GalaxyProfile.DISC, + 900d, 2200d, 0.02d, 0, 220d, 0.08d, 7); + } + + private static GalaxyGenConfig.GalaxyType dwarf() { + return new GalaxyGenConfig.GalaxyType("Dwarf", GalaxyGenConfig.GalaxyProfile.SPHEROID, + 120d, 500d, 0.70d, 0, 20d, 0.90d, 700); + } + + /** A galaxy with its plane on the world's XZ plane, so a test can reason in plain coordinates. */ + private static Galaxy flat(GalaxyGenConfig.GalaxyType type) { + return new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, type, RADIUS, 0d, 0d, + Math.toRadians(20d), 0d); + } + + @Test + public void theBoundaryIsTheDeclaredRadius() { + // Not a level of the profile. A profile is continuous and has no boundary, so a frame decided + // by "is it dense enough here" would flip for anything hovering on the threshold — and the + // frame decides whether a thing rotates with the galaxy or is carried by the void. + Galaxy g = flat(spiral()); + assertTrue(g.contains(RADIUS * 0.999d, 0d, 0d)); + assertFalse(g.contains(RADIUS * 1.001d, 0d, 0d)); + // It is a SPHERE, so the halo well above a thin disc is still inside the galaxy. + assertTrue("the halo above a disc is bound to the galaxy too", g.contains(0d, RADIUS * 0.9d, 0d)); + assertEquals("and there are no stars out there", 0d, g.densityAt(RADIUS * 1.5d, 0d, 0d), 0d); + } + + @Test + public void densityFallsWithRadiusAndWithHeight() { + Galaxy g = flat(smoothDisc()); + double centre = g.densityAt(0d, 0d, 0d); + double midway = g.densityAt(RADIUS * 0.4d, 0d, 0d); + double rim = g.densityAt(RADIUS * 0.9d, 0d, 0d); + assertTrue("the nucleus is the densest point", centre > midway); + assertTrue("and it keeps thinning outwards", midway > rim); + + // Off the plane at the same radius: a disc is a disc. + double inPlane = g.densityAt(RADIUS * 0.4d, 0d, 0d); + double aloft = g.densityAt(RADIUS * 0.4d, RADIUS * 0.1d, 0d); + assertTrue("a disc must thin out of its plane (" + inPlane + " vs " + aloft + ")", + inPlane > aloft); + } + + @Test + public void aDiscIsFlatterThanItIsWide() { + // The one claim that separates a disc from a sphere: the same fraction of the radius costs + // far more density vertically than radially. + Galaxy g = flat(smoothDisc()); + double outward = g.densityAt(RADIUS * 0.05d, 0d, 0d); + double upward = g.densityAt(0d, RADIUS * 0.05d, 0d); + assertTrue("going up must cost more than going out (" + upward + " vs " + outward + ")", + upward < outward); + } + + @Test + public void armsModulateTheDiscTheyDoNotGateIt() { + // An arm is where a disc is denser, not where it exists. If the between-arm density were zero + // the galaxy would be a set of curves rather than a disc with structure in it. + Galaxy armed = flat(spiral()); + double min = Double.MAX_VALUE; + double max = 0d; + double r = RADIUS * 0.5d; + for (int i = 0; i < 360; i++) { + double theta = Math.toRadians(i); + double d = armed.densityAt(r * Math.cos(theta), 0d, r * Math.sin(theta)); + min = Math.min(min, d); + max = Math.max(max, d); + } + assertTrue("arms must make the disc vary with angle", max > min); + assertTrue("but between the arms there are still stars", min > 0d); + } + + @Test + public void aTypeWithNoArmsIsAxisymmetric() { + // The no-arms case is the same code path with an empty term, so a smooth disc has to come out + // genuinely smooth rather than nearly so. + Galaxy smooth = flat(smoothDisc()); + double r = RADIUS * 0.5d; + double reference = smooth.densityAt(r, 0d, 0d); + for (int i = 0; i < 360; i += 15) { + double theta = Math.toRadians(i); + assertEquals("a smooth disc must not vary with angle", reference, + smooth.densityAt(r * Math.cos(theta), 0d, r * Math.sin(theta)), 1e-12d); + } + } + + @Test + public void orientationRotatesTheDiscWithoutChangingItsShape() { + // Two galaxies alike but for their orientation must be the same object seen from elsewhere: + // the density a point sees depends on where it is IN THE GALAXY, never on the world axes. + Galaxy flat = new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, 0d, 0d, + Math.toRadians(20d), 0d); + Galaxy tilted = new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, + Math.toRadians(90d), 0d, Math.toRadians(20d), 0d); + // The tilted galaxy's pole is +X, so ITS plane is the world's YZ plane. + double r = RADIUS * 0.3d; + assertEquals("the same point of the galaxy must read the same however it is oriented", + flat.densityAt(r, 0d, 0d), tilted.densityAt(0d, 0d, r), 1e-12d); + assertEquals("and so must its pole", flat.densityAt(0d, r, 0d), tilted.densityAt(r, 0d, 0d), + 1e-12d); + } + + @Test + public void rotationIsSolidBodyInTheCoreAndShearsOutside() { + // omega(r) constant means no shear; omega falling with r IS the shear. A galaxy that sheared + // nowhere would carry its arms round rigidly forever, and one that sheared everywhere would + // tear its own nucleus apart. + Galaxy g = flat(spiral()); + double core = RADIUS * spiral().coreRadiusFraction; + assertEquals("well inside the core the curve is solid-body, so omega is flat", + g.angularSpeedAt(core * 0.001d), g.angularSpeedAt(core * 0.01d), + g.angularSpeedAt(0d) * 1e-3d); + assertTrue("outside the core, omega must fall with radius", + g.angularSpeedAt(RADIUS * 0.9d) < g.angularSpeedAt(RADIUS * 0.3d)); + assertTrue("and it is finite at the very centre", g.angularSpeedAt(0d) > 0d + && !Double.isInfinite(g.angularSpeedAt(0d))); + } + + @Test + public void aDwarfShearsLessThanAMassiveSpiral() { + // The type earns its keep here, and it is what a real rotation curve does: a dwarf turns + // nearly as a solid body while a massive spiral's curve is flat and shears strongly. Measured + // as the ratio of omega across the same FRACTIONAL radii, so it compares shapes, not speeds. + Galaxy small = flat(dwarf()); + Galaxy big = flat(spiral()); + double dwarfShear = small.angularSpeedAt(RADIUS * 0.2d) / small.angularSpeedAt(RADIUS * 0.8d); + double spiralShear = big.angularSpeedAt(RADIUS * 0.2d) / big.angularSpeedAt(RADIUS * 0.8d); + assertTrue("a dwarf must shear less than a spiral (" + dwarfShear + " vs " + spiralShear + ")", + dwarfShear < spiralShear); + } + + @Test + public void thetaIsEvaluatedNeverIntegrated() { + // Analytic in t: theta at 2t must be exactly theta0 plus twice the advance, with no drift a + // step-by-step accumulation would build up. + Galaxy g = flat(spiral()); + double r = RADIUS * 0.5d; + double theta0 = 1.234d; + double advance = g.thetaAt(theta0, r, 1_000_000L) - theta0; + assertEquals(theta0 + 2d * advance, g.thetaAt(theta0, r, 2_000_000L), 1e-12d); + } + + @Test + public void rotationIsSlowEnoughToBeInvisibleWithinASave() { + // Recorded as a measurement, not a requirement: the mechanic exists even when slow, and the + // speed is tuning. What this pins is that the law is expressed in the SAME clock the game + // counts in — a period that came out in ticks-per-turn of order one would mean the km/s + // conversion had lost a calendar somewhere. + Galaxy g = flat(spiral()); + double turnTicks = g.rotationPeriodTicks(RADIUS * 0.5d); + assertTrue("a galactic turn must dwarf any play session (" + turnTicks + " ticks)", + turnTicks > 1e11d); + assertFalse("but it must be a finite number of ticks", Double.isInfinite(turnTicks)); + } + + @Test + public void aSectorReadingAgreesWithTheLengthItStandsFor() { + // The generator asks in cell names; everything above is written in light years. The two have + // to be the same question, or the star field would be placed by one metric and bounded by + // another — which is the failure this whole layer keeps removing. + Galaxy g = flat(smoothDisc()); + long cells = UniverseScale.cellsForLightYears(RADIUS * 0.5d); + assertEquals(g.densityAt(UniverseScale.lightYearsForCells(cells), 0d, 0d), + g.densityAtSector(cells, 0L, 0L), 1e-12d); + assertFalse("and a sector past the radius is outside", + g.containsSector(UniverseScale.cellsForLightYears(RADIUS * 1.5d), 0L, 0L)); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java index d277b2f39..826822f7e 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java @@ -83,7 +83,7 @@ public void theNearestSystemIsFarEnoughToBeAJumpAndCloseEnoughToBeReached() { .append(BASELINE_SPEED).append(" blocks/tick ===\n"); report.append("cell edge ").append(GalacticCoord.CELL).append(" blocks, minSpacing ") .append(cfg.minSpacing).append(" cells, density ").append(cfg.density) - .append(", clusterScale ").append(cfg.clusterScale).append('\n'); + .append(", galaxy spacing ").append(cfg.galaxySpacing).append(" cells\n"); for (String row : rows) { report.append(" ").append(row).append('\n'); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java index a89f3c3e8..2b2106d1f 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java @@ -48,7 +48,8 @@ public void resetSeams() { private static UniverseRegistry registryWithProceduralGalaxy() { UniverseRegistry reg = new UniverseRegistry(); UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator( - new GalaxyGenConfig(1.0d, SPACING, 8, 0.0d, null))); + new GalaxyGenConfig(SPACING, 1.0d, GalaxyGenConfig.DEFAULT_GALAXY_SPACING, + GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, null, null))); reg.bindWorldSeed(SEED); return reg; } @@ -274,7 +275,8 @@ public void aPinnedSystemsStarSurvivesAChangeOfGenerator() { // A pack edit: a different spacing, a different density, a whole different galaxy. UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator( - new GalaxyGenConfig(0.2d, SPACING / 2, 4, 0.5d, null))); + new GalaxyGenConfig(SPACING / 2, 0.2d, GalaxyGenConfig.DEFAULT_GALAXY_SPACING, + GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, null, null))); Optional after = reg.starAt(cell); assertTrue(after.isPresent()); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java index b489ae080..0b575ede1 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java @@ -62,7 +62,9 @@ private static GalacticCoord cell(long sx, long sy, long sz) { /** A galaxy dense enough to sample: every cube occupied, so a small sweep finds many systems. */ private static ClusteredGalaxyGenerator gen(int minSpacing) { - return new ClusteredGalaxyGenerator(new GalaxyGenConfig(0.9d, minSpacing, 8, 0.0d, null)); + return new ClusteredGalaxyGenerator(new GalaxyGenConfig(minSpacing, 0.9d, + GalaxyGenConfig.DEFAULT_GALAXY_SPACING, GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, + null, null)); } /** Every occupied system anchor in a sweep of super-cells. */ diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java index 611927cb3..fa6fa58af 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java @@ -282,7 +282,8 @@ public void memberCellResolvesToItsOwningProceduralSystem() { // system; the zone read returns exactly that cell's body. UniverseRegistry reg = new UniverseRegistry(); reg.bindWorldSeed(0xBEEF); - GalaxyGenConfig cfg = new GalaxyGenConfig(0.9d, 16, 8, 0.0d, null); + GalaxyGenConfig cfg = new GalaxyGenConfig(16, 0.9d, GalaxyGenConfig.DEFAULT_GALAXY_SPACING, + GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, null, null); UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(cfg)); // Find an occupied super-cell and a non-star body of its system. @@ -333,7 +334,8 @@ public void memberCellResolvesToItsOwningProceduralSystem() { public void pinOnTouchSnapshotsAProceduralSystemAgainstSeedChange() { UniverseRegistry reg = new UniverseRegistry(); reg.bindWorldSeed(1234L); - GalaxyGenConfig cfg = new GalaxyGenConfig(0.9d, 8, 8, 0.0d, null); + GalaxyGenConfig cfg = new GalaxyGenConfig(8, 0.9d, GalaxyGenConfig.DEFAULT_GALAXY_SPACING, + GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, null, null); UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(cfg)); GalacticCoord anchor = null; @@ -506,7 +508,9 @@ public void bodiesAtIsEmptyOnVoidCellWithDefaultGenerator() { public void bodiesAtMergesProceduralBodiesAndPois() { UniverseRegistry reg = new UniverseRegistry(); reg.bindWorldSeed(0xABCDEFL); - UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(new GalaxyGenConfig(0.9d, 1, 8, 0.0d, null))); + UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(new GalaxyGenConfig(1, 0.9d, + GalaxyGenConfig.DEFAULT_GALAXY_SPACING, GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, + null, null))); GalacticCoord found = null; for (long x = 0; x < 300 && found == null; x++) { From 36c0731b1d63561866add7a60b1a1d1005b01552 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Fri, 14 Aug 2026 18:42:36 +0300 Subject: [PATCH 17/42] feat: the void gets a frame, galaxies get clusters and a place in time - Expansion and peculiar motion carry a galaxy's centre, not its insides - A position is bound to its galaxy or comoving in the void, never both - Star clusters refine the lattice by an exact commensurate subdivision - Authored anchors are declared against a galaxy, not an absolute cell - Galaxy archetypes become authorable, disc thickness among them - Seat the home galaxy around the origin so it is not the nucleus --- docs/README_PLANETDEFS.md | 68 +++- .../command/test/TestProbeCommand.java | 2 +- .../universe/ClusterField.java | 148 +++++++++ .../universe/ClusteredGalaxyGenerator.java | 304 ++++++++++++++---- .../advancedRocketry/universe/Cosmology.java | 72 +++++ .../universe/GalacticAnchor.java | 78 +++++ .../universe/GalacticFrame.java | 35 ++ .../advancedRocketry/universe/Galaxy.java | 159 +++++++-- .../universe/GalaxyField.java | 200 ++++++++++-- .../universe/GalaxyGenConfig.java | 98 ++++++ .../advancedRocketry/universe/GalaxyKey.java | 106 ++++++ .../universe/IGalaxyGenerator.java | 21 ++ .../universe/LightYearVector.java | 83 +++++ .../universe/StarCluster.java | 117 +++++++ .../universe/UniverseRegistry.java | 64 +++- .../universe/UniverseScale.java | 50 ++- .../util/XMLPlanetLoader.java | 188 ++++++++++- .../test/integration/SystemContentTest.java | 10 +- .../test/integration/XMLPlanetLoaderTest.java | 104 ++++++ .../test/unit/GalaxyFieldTest.java | 214 +++++++++++- .../test/unit/GalaxyTest.java | 82 ++++- .../test/unit/StarClusterTest.java | 252 +++++++++++++++ .../test/unit/UniverseRegistryTest.java | 15 +- 23 files changed, 2297 insertions(+), 173 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/Cosmology.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/GalacticAnchor.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/GalacticFrame.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/GalaxyKey.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/LightYearVector.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/StarCluster.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java diff --git a/docs/README_PLANETDEFS.md b/docs/README_PLANETDEFS.md index 45952fa8b..e755e27a7 100644 --- a/docs/README_PLANETDEFS.md +++ b/docs/README_PLANETDEFS.md @@ -70,7 +70,7 @@ So: edit the **template**, not the live copy, and keep the template under versio | **planet temperature** | KELVIN | Computed, not authored — see `avgTemperature` in §7. | | **rotational period** | ticks | `24000` = one Minecraft day. Must be `> 0`. | | **star map position** | arbitrary map units | `x` / `y` on ``; affects the star-selector GUI only. | -| **galactic anchor** | cell indices | `"sectorX,sectorY,sectorZ"`. One cell is 4 000 000 blocks. | +| **galactic anchor** | cell indices | `"sectorX,sectorY,sectorZ"`, GALAXY-LOCAL (see §5). One cell is 4 000 000 blocks. | **The chart scale.** One orbital-distance unit is **5 983 914 blocks**, i.e. one AU is 149 597 870 700 m at 250 m per block. This is the one law that turns an orbit into a place, and it is @@ -85,7 +85,8 @@ distance and where a ship actually finds it are the same statement. ```xml - + + @@ -110,7 +111,7 @@ this file names. | attribute | unit | default | meaning | |---|---|---|---| -| `density` | 0..1 | `0.35` | Chance that a given cube of space holds a system **at a galaxy's densest point**. Everywhere else the galaxy's own profile scales it down, and outside every galaxy it is zero. Clamped; `NaN` reads as `0`. | +| `density` | 0..1 | `0.35` | Chance that a given cube of space holds a system **in a sun-like part of a galaxy's disc** — the profile is normalised there, so this number describes the sky you actually stand under. Nearer the centre it rises (and saturates); further out and off the plane it falls; outside every galaxy it is zero. Clamped; `NaN` reads as `0`. | | `minSpacing` | cells | `40018890` | Edge of the cube that holds **at most one** system, i.e. how far apart stars stand. The default is 4.23 light years. Floors at 1. | | `galaxySpacing` | cells | `709554785444` | Edge of the cube that holds **at most one galaxy**. The default is 75 000 light years — twenty-five galaxy diameters. Floors at 1. | | `galaxyDensity` | 0..1 | `0.5` | Fraction of those cubes that actually hold a galaxy. The rest is intergalactic void. Clamped; `NaN` reads as `0`. | @@ -126,14 +127,58 @@ galaxy's edge. A galaxy's **type decides its size**, never the other way round: dwarf spheroidals and dwarf irregulars outnumber spirals and ellipticals by roughly two orders, so finding a spiral is an event. -The archetype table is built in and is not authorable yet. +The archetype table is `` below. -**The galaxy at the origin always exists.** Authored `` anchors are absolute -coordinates, and a galaxy fills a ten-thousandth of its own cube — so without a reserved home the -system you write in this file would land in intergalactic space on virtually every seed. The home -galaxy is centred on the origin and is always drawn large enough (at least 800 light years) to hold -authored content; only its *existence* and its centre are fixed, so its type, size, orientation and -arms still differ from seed to seed. +**Clusters, one level down.** Inside a star cluster the lattice is finer by an integer factor, so a +cluster really is denser than the field around it rather than merely looking that way. Every galaxy +also has a nucleus at its own centre — the richest cluster of all, and not a special case. A +consequence worth knowing: **the 10 000 AU separation floor is a property of the lattice level, not a +global constant.** Inside a cluster stars stand closer than a wide binary, and a system there keeps +fewer outer bodies, by the same rule that applies everywhere else. + +### Where authored content goes — `galaxy` and `galacticCoord` + +A ``'s `galacticCoord` is **galaxy-local**: an offset in cells from the DECLARATION ORIGIN of the +galaxy named by its `galaxy` attribute. + +| attribute | default | meaning | +|---|---|---| +| `galaxy` | `home` | `home`, or a lattice index `"gx,gy,gz"`. **Naming a galaxy forces that cell to hold one**, on every seed. | +| `galacticCoord` | *(absent → a deterministic fallback cell)* | `"sx,sy,sz"` — the offset from that galaxy's declaration origin. | + +For `home` the declaration origin is the **universe origin**, so a coordinate written before galaxies +existed means exactly what it always did. For any other galaxy it is that galaxy's centre. + +**The home galaxy always exists, and the origin sits out in its disc.** A galaxy fills about three +thousandths of a percent of its own lattice cell, so an absolute declaration would land in +intergalactic space with probability 99.997 %. The home galaxy is therefore seated *around* the origin +— not *on* it, because a galaxy's centre is its nucleus and that is the last address a shipped solar +system should have. The origin lands at a sun-like galactic radius, in the plane. + +**Anything within about 400 light years of the origin is valid on every seed.** That is what the +guaranteed minimum radius leaves once the origin has been moved off centre. Beyond it your system is +inside its galaxy on some seeds and in the void on others; you get a loud error in the log naming the +star, never a silent clamp. + +### `` — the galaxy archetype table + +Zero `` children → the built-in table stands. One or more → they **replace** it entirely. +Every attribute defaults to the stock spiral's value, so changing only how flat a disc is takes one +attribute. + +| attribute | unit | default | meaning | +|---|---|---|---| +| `name` | text | `Galaxy` | Shown in a galaxy's designation. | +| `profile` | `DISC` / `SPHEROID` | `DISC` | The shape stars are distributed in. A `SPHEROID` has no plane, so no arms. | +| `minRadius` / `maxRadius` | light years | `900` / `2200` | Radius band. A galaxy's radius is drawn INSIDE ITS TYPE'S band — size and type are one fact, not two. | +| `thickness` | fraction of the radius | `0.02` | Scale height: how flat it is. `0.02` is a real thin disc (a 30-light-year scale height at the stock radius); raise it to make leaving the disc a manoeuvre rather than a step. On a `SPHEROID` it is the flattening of the pole. | +| `arms` | count | `2` | Spiral arms, or `0` for a type that has none. | +| `rotationSpeed` | km/s | `220` | The rotation curve's asymptotic speed. | +| `coreFraction` | fraction of the radius | `0.08` | Where the rotation curve turns over. Near `1` the galaxy turns almost as a solid body; near `0` its curve is flat almost everywhere and it shears strongly. | +| `weight` | relative | `1` | Draw weight. | + +The stock table is roughly the real abundance ordering — dwarf spheroidals and dwarf irregulars +outnumber spirals and ellipticals by about two orders — so a spiral is something you find. ### `` — the archetype table @@ -247,7 +292,8 @@ redistributed among the remaining options. A type all of whose options are unava | `temp` | `100` = Sol | no (default `100`) | Temperature; drives colour and luminosity. A malformed value warns and falls back to `100`. | | `size` | solar radii | no (default `1.0`) | Radius. | | `x`, `y` | map units | no | Position on the star-selector map. `y` is the map's Z. | -| `galacticCoord` | `"sx,sy,sz"` | no | Explicit anchor cell. Malformed → warns and uses the origin. Absent → a deterministic fallback cell is assigned. | +| `galacticCoord` | `"sx,sy,sz"` | no | Explicit anchor, GALAXY-LOCAL — an offset from the declaration origin of the galaxy in `galaxy` (see §5). Malformed → warns and uses the origin. Absent → a deterministic fallback cell is assigned. | +| `galaxy` | `home` or `"gx,gy,gz"` | no | Which galaxy `galacticCoord` is measured from. Default `home`, whose declaration origin IS the universe origin. Naming any other forces that lattice cell to hold a galaxy. | | `numPlanets` | count | **yes** | How many random planets to generate for this star at FIRST load. Missing → warning and none. | | `numGasGiants` | count | **yes** | The same for gas giants. | | `blackHole` | boolean | no | This star is a black hole: a quarter of the light its size and temperature would otherwise give. | diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 27c532d22..14d688497 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -4791,7 +4791,7 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] return; } long r = parseIntOr(args[1], 8); - long s = Math.max(1L, zmaster587.advancedRocketry.universe.UniverseRegistry.generator() + long s = Math.max(1L, zmaster587.advancedRocketry.universe.UniverseRegistry.getGenerator() .minSpacingCells()); for (long x = -r; x <= r; x++) { for (long y = -r; y <= r; y++) { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java new file mode 100644 index 000000000..003f37c57 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java @@ -0,0 +1,148 @@ +package zmaster587.advancedRocketry.universe; + +import java.util.Optional; + +/** + * Where the star clusters are: the seat one level BELOW the star lattice, inside a galaxy. + * + *

Space is partitioned into cluster cells {@code CLUSTER_SPACING_LY} across, measured in COARSE + * super-cells; at most one cluster per cell, seated with a margin of its own radius so it never + * straddles a face. That containment is what keeps "which cluster is this super-cell in" a single hash + * lookup with one answer, exactly as it does one and two levels up.

+ * + *

Plus one cluster that is not on the lattice at all: every galaxy has a nucleus at its own + * centre. It is not a special case in the code either — it is a cluster of a different type, + * seated at a known place instead of a drawn one.

+ * + *

A cluster only exists where its galaxy has stars: occupancy is scaled by the same density profile + * that placed the systems, so clusters thin out and stop where the galaxy does.

+ */ +public final class ClusterField { + + // Its own salt space again, clear of the galaxy tier's and of the generator's. + private static final long SALT_CLUSTER_OCC = 0x201L; + private static final long SALT_CLUSTER_TYPE = 0x202L; + private static final long SALT_CLUSTER_RADIUS = 0x203L; + private static final long SALT_CLUSTER_OX = 0x204L; + private static final long SALT_CLUSTER_OY = 0x205L; + private static final long SALT_CLUSTER_OZ = 0x206L; + private static final long SALT_NUCLEUS_RADIUS = 0x207L; + + private final GalaxyGenConfig config; + private final long spacingSuperCells; + private final long totalClusterWeight; + + public ClusterField(GalaxyGenConfig config) { + this.config = (config == null) ? GalaxyGenConfig.defaults() : config; + this.spacingSuperCells = Math.max(1L, + superCellsForLightYears(GalaxyGenConfig.CLUSTER_SPACING_LY, this.config.minSpacing)); + long w = 0L; + for (GalaxyGenConfig.ClusterType t : this.config.clusterTypes) { + w += t.weight; + } + this.totalClusterWeight = Math.max(1L, w); + } + + /** + * The cluster this coarse super-cell belongs to, or empty when it is ordinary field. + * + *

{@code galaxy} is the galaxy that owns the super-cell; a cluster outside a galaxy is not a + * thing this generator makes, because there would be no stars to gather.

+ */ + public Optional clusterAt(long seed, Galaxy galaxy, long supX, long supY, long supZ) { + if (galaxy == null) { + return Optional.empty(); + } + Optional nucleus = nucleusOf(seed, galaxy); + if (nucleus.isPresent() && nucleus.get().containsSuperCell(supX, supY, supZ)) { + return nucleus; + } + long cx = Math.floorDiv(supX, spacingSuperCells); + long cy = Math.floorDiv(supY, spacingSuperCells); + long cz = Math.floorDiv(supZ, spacingSuperCells); + Optional seated = clusterAtIndex(seed, galaxy, cx, cy, cz); + if (seated.isPresent() && seated.get().containsSuperCell(supX, supY, supZ)) { + return seated; + } + return Optional.empty(); + } + + /** The galactic nucleus: the richest cluster, at the galaxy's own centre. */ + public Optional nucleusOf(long seed, Galaxy galaxy) { + if (galaxy == null) { + return Optional.empty(); + } + double u = CellHash.norm(CellHash.of(seed, galaxy.cellX(), galaxy.cellY(), galaxy.cellZ(), + SALT_NUCLEUS_RADIUS)); + GalaxyGenConfig.ClusterType type = GalaxyGenConfig.NUCLEUS; + double radiusLy = type.minRadiusLy + u * (type.maxRadiusLy - type.minRadiusLy); + long s = config.minSpacing; + return Optional.of(new StarCluster(type, + Math.floorDiv(galaxy.centre().sectorX(), s), + Math.floorDiv(galaxy.centre().sectorY(), s), + Math.floorDiv(galaxy.centre().sectorZ(), s), + superCellsForLightYears(radiusLy, config.minSpacing))); + } + + /** + * The cluster seated in cluster cell {@code (cx, cy, cz)}, or empty. + * + *

Occupancy is scaled by the galaxy's own density profile at the cell, so clusters live where + * stars live and stop where the galaxy stops — one function, not a second rule.

+ */ + public Optional clusterAtIndex(long seed, Galaxy galaxy, long cx, long cy, long cz) { + long s = config.minSpacing; + // The cluster cell's centre, as a sector triple, so the profile is read at a fixed point. + long centreSuper = spacingSuperCells / 2L; + long sectorX = (cx * spacingSuperCells + centreSuper) * s; + long sectorY = (cy * spacingSuperCells + centreSuper) * s; + long sectorZ = (cz * spacingSuperCells + centreSuper) * s; + double profile = galaxy.densityAtSector(sectorX, sectorY, sectorZ); + if (!(profile > 0d)) { + return Optional.empty(); + } + if (CellHash.norm(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OCC)) + >= Math.min(1d, GalaxyGenConfig.CLUSTER_DENSITY * profile)) { + return Optional.empty(); + } + + GalaxyGenConfig.ClusterType type = pickType(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_TYPE)); + double radiusFraction = CellHash.norm(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_RADIUS)); + double radiusLy = type.minRadiusLy + radiusFraction * (type.maxRadiusLy - type.minRadiusLy); + long radius = superCellsForLightYears(radiusLy, config.minSpacing); + + // Seated with a margin of its own radius, so a cluster never straddles a cluster-cell face and + // the ownership question stays a single lookup. + long margin = Math.min(radius, Math.max(0L, (spacingSuperCells - 1L) / 2L)); + long band = Math.max(1L, spacingSuperCells - 2L * margin); + long ox = margin + Math.floorMod(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OX), band); + long oy = margin + Math.floorMod(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OY), band); + long oz = margin + Math.floorMod(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OZ), band); + return Optional.of(new StarCluster(type, cx * spacingSuperCells + ox, + cy * spacingSuperCells + oy, cz * spacingSuperCells + oz, radius)); + } + + /** The cluster-lattice edge, in coarse super-cells. */ + public long spacingSuperCells() { + return spacingSuperCells; + } + + /** A length in light years as a whole number of coarse super-cells, at least one. */ + private static long superCellsForLightYears(double lightYears, long superCellEdgeCells) { + long cells = UniverseScale.cellsForLightYears(lightYears); + return Math.max(1L, cells / Math.max(1L, superCellEdgeCells)); + } + + private GalaxyGenConfig.ClusterType pickType(long h) { + long r = Math.floorMod(h, totalClusterWeight); + GalaxyGenConfig.ClusterType last = null; + for (GalaxyGenConfig.ClusterType t : config.clusterTypes) { + last = t; + if (r < t.weight) { + return t; + } + r -= t.weight; + } + return last; // config.clusterTypes is never empty + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 5ffe254f7..b997bf92d 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -163,13 +163,25 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { /** Thin-disk half-thickness as a fraction of the orbit radius (bodies keep honest 3D Y). */ private static final double PROC_DISK_FRACTION = 0.1d; + private static final org.apache.logging.log4j.Logger LOGGER = + org.apache.logging.log4j.LogManager.getLogger("AdvancedRocketry|Universe"); + + /** + * Hard ceiling on what one region query returns. Not a balance number: a nucleus divides each + * coarse cell fifteen thousand ways, so a box that looks small in super-cells can hold millions of + * systems and an unbounded enumeration would hang the caller. + */ + private static final int MAX_SYSTEMS_PER_REGION_QUERY = 20_000; + private final GalaxyGenConfig config; private final GalaxyField galaxies; + private final ClusterField clusters; private final long totalStarWeight; public ClusteredGalaxyGenerator(GalaxyGenConfig config) { this.config = (config == null) ? GalaxyGenConfig.defaults() : config; this.galaxies = new GalaxyField(this.config); + this.clusters = new ClusterField(this.config); long w = 0L; // accumulate in long so a few near-Integer.MAX weights cannot overflow the sum for (GalaxyGenConfig.StarType t : this.config.starTypes) { w += t.weight; @@ -186,14 +198,15 @@ public GalaxyField galaxies() { return galaxies; } + /** The star clusters that refine the lattice — the tier below it. */ + public ClusterField clusters() { + return clusters; + } + @Override public Optional systemAt(long seed, GalacticCoord coord) { - long sx = coord.sectorX(); - long sy = coord.sectorY(); - long sz = coord.sectorZ(); - long s = config.minSpacing; - Optional g = systemForSuperCell(seed, - Math.floorDiv(sx, s), Math.floorDiv(sy, s), Math.floorDiv(sz, s)); + Optional g = systemForLattice(seed, + latticeAt(seed, coord.sectorX(), coord.sectorY(), coord.sectorZ())); if (g.isPresent() && g.get().cell.sameCell(coord)) { return Optional.of(g.get().system); } @@ -203,8 +216,14 @@ public Optional systemAt(long seed, GalacticCoord coord) { /** * {@inheritDoc} * - *

Cost is O(super-cell volume of the box). Callers MUST pass a bounded region — a telescope scan is - * range-limited by config — not a galactic-scale box.

+ *

Cost is O(super-cell volume of the box), times {@code k³} for any part of it inside a star + * cluster. Callers MUST pass a bounded region — a telescope scan is range-limited by config — not a + * galactic-scale box.

+ * + *

The result is capped, and a cap that fires is LOGGED. A nucleus subdivides each coarse + * cell fifteen thousand ways, so a box that looks small in super-cells can hold millions of + * systems; silently returning the first few would read as "that is all there is", which is the one + * outcome worse than a slow scan.

*/ @Override public Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { @@ -217,22 +236,37 @@ public Map systemsInRegion(long seed, GalacticCoord m long hiZ = Math.max(min.sectorZ(), max.sectorZ()); Map out = new HashMap<>(); - for (long supX = Math.floorDiv(loX, s); supX <= Math.floorDiv(hiX, s); supX++) { - for (long supY = Math.floorDiv(loY, s); supY <= Math.floorDiv(hiY, s); supY++) { - for (long supZ = Math.floorDiv(loZ, s); supZ <= Math.floorDiv(hiZ, s); supZ++) { - Optional g = systemForSuperCell(seed, supX, supY, supZ); - if (!g.isPresent()) { - continue; - } - GalacticCoord c = g.get().cell; - if (c.sectorX() >= loX && c.sectorX() <= hiX - && c.sectorY() >= loY && c.sectorY() <= hiY - && c.sectorZ() >= loZ && c.sectorZ() <= hiZ) { - out.put(c, g.get().system); + boolean capped = false; + for (long supX = Math.floorDiv(loX, s); supX <= Math.floorDiv(hiX, s) && !capped; supX++) { + for (long supY = Math.floorDiv(loY, s); supY <= Math.floorDiv(hiY, s) && !capped; supY++) { + for (long supZ = Math.floorDiv(loZ, s); supZ <= Math.floorDiv(hiZ, s) && !capped; supZ++) { + int k = subdivisionAt(seed, supX, supY, supZ); + for (long i = 0; i < k && !capped; i++) { + for (long j = 0; j < k && !capped; j++) { + for (long m = 0; m < k && !capped; m++) { + Optional g = systemForLattice(seed, + Lattice.of(supX, supY, supZ, i, j, m, k, s)); + if (!g.isPresent()) { + continue; + } + GalacticCoord c = g.get().cell; + if (c.sectorX() >= loX && c.sectorX() <= hiX + && c.sectorY() >= loY && c.sectorY() <= hiY + && c.sectorZ() >= loZ && c.sectorZ() <= hiZ) { + out.put(c, g.get().system); + capped = out.size() >= MAX_SYSTEMS_PER_REGION_QUERY; + } + } + } } } } } + if (capped) { + LOGGER.warn("systemsInRegion stopped at " + MAX_SYSTEMS_PER_REGION_QUERY + " systems for the" + + " box " + min.cellKey() + " .. " + max.cellKey() + "; there are more. This region" + + " crosses a dense star cluster - narrow the query."); + } return out; } @@ -259,7 +293,12 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { // uses. What the neighbourhood decides is not how far a body goes but how many bodies there is // room for: orbits are drawn inside a bracket that already fits, and a system that would run // past its own clear space loses BODIES rather than being squashed to fit. - long s = config.minSpacing; + // + // The room is the LOCAL lattice cell's, not the coarse one's. A system inside a star cluster + // sits on a finer lattice, so it has less of it and keeps fewer named bodies — which is the + // same rule as everywhere else, applied to the level it is defined on. + Lattice lattice = latticeAt(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ()); + long s = lattice.minEdge(); double outerBound = maxNamedOrbitUnits(s); // AT MOST ONE REAL BODY PER CELL, moons excepted. The draw picks each body's angle and radius @@ -281,7 +320,7 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY * AstronomicalBodyHelper.getOrbitalPeriod(companion.getOrbitalDistance(), star.getMass()); - Seat seat = claimSeat(cell, s, taken, companion.getOrbitalDistance(), + Seat seat = claimSeat(cell, lattice, taken, companion.getOrbitalDistance(), companion.getBaseTheta(), 0d, periodTicks); if (seat == null) { continue; @@ -312,7 +351,7 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { if (!orbitIsStableAmong(star.getSubStars(), orbit)) { continue; // too near one of this system's other stars for any orbit to survive } - Seat seat = seatBody(seed, cell, i, orbit, star, s, taken); + Seat seat = seatBody(seed, cell, i, orbit, star, lattice, taken); if (seat == null) { continue; // this system's neighbourhood is full — a bound of the layout, not a failure } @@ -341,7 +380,7 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { // An inner belt is DERIVED from a giant and never rolled: it is material a giant's resonances // stopped from accreting, so it belongs in the gap inside one and a system with no giant has none. if (innermostGiantOrbit > 0) { - addBelt(bodies, seed, cell, (int) (innermostGiantOrbit / INNER_BELT_RESONANCE), star, s, + addBelt(bodies, seed, cell, (int) (innermostGiantOrbit / INNER_BELT_RESONANCE), star, lattice, starId, taken, count + 1); } // The outer belt is MANDATORY on every system — the Kuiper analogue, and the reason every system @@ -353,7 +392,7 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { // it is bounded like everything else rather than being quietly dropped. double outerBelt = Math.max(outermostOrbit * OUTER_BELT_FACTOR, PlanetDerivation.innerOrbit(star) * 2d); - addBelt(bodies, seed, cell, (int) Math.min(outerBelt, outerBound), star, s, starId, taken, + addBelt(bodies, seed, cell, (int) Math.min(outerBelt, outerBound), star, lattice, starId, taken, count + 2); return bodies; } @@ -396,7 +435,7 @@ private static double maxNamedOrbitUnits(long s) { * one outcome that is worse than a smaller system.

*/ private static Seat seatBody(long seed, GalacticCoord anchor, int index, int orbit, - StellarBody star, long s, Set taken) { + StellarBody star, Lattice lattice, Set taken) { double baseAngle = CellHash.norm(CellHash.ofBody(seed, anchor, index, SALT_BODYANG)) * 2d * Math.PI; // Out-of-plane displacement lives in the LAW as an inclination, so a body's height above the // disk is part of where it IS at every tick rather than a one-off nudge applied to its name. @@ -405,11 +444,11 @@ private static Seat seatBody(long seed, GalacticCoord anchor, int index, int orb double phiDegrees = Math.toDegrees(Math.asin(sinPhi)); double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY * AstronomicalBodyHelper.getOrbitalPeriod(orbit, star.getMass()); - return claimSeat(anchor, s, taken, orbit, baseAngle, phiDegrees, periodTicks); + return claimSeat(anchor, lattice, taken, orbit, baseAngle, phiDegrees, periodTicks); } /** Walk the ring from {@code baseAngle} until a free cell turns up, or give up. */ - private static Seat claimSeat(GalacticCoord anchor, long s, Set taken, int orbit, + private static Seat claimSeat(GalacticCoord anchor, Lattice lattice, Set taken, int orbit, double baseAngle, double phiDegrees, double periodTicks) { for (int attempt = 0; attempt < NUDGE_ATTEMPTS; attempt++) { BodyEphemeris law = BodyEphemeris.orbit(orbit, baseAngle + attempt * NUDGE_ANGLE, @@ -418,8 +457,8 @@ private static Seat claimSeat(GalacticCoord anchor, long s, Set taken, i // The body's address is its OWN cell's centre (zone content sits near the cell centre), // box-clamped into the anchor's super-cell so member attribution stays exact at ANY // spacing — at tiny spacings a whole orbit can otherwise reach across the super-cell face. - GalacticCoord addr = clampIntoSuperCell( - anchor.plusLocal(at0.dx(), at0.dy(), at0.dz()).cellCentre(), anchor, s); + GalacticCoord addr = clampIntoLattice( + anchor.plusLocal(at0.dx(), at0.dy(), at0.dz()).cellCentre(), lattice); if (taken.add(addr.cellKey())) { return new Seat(addr, law); } @@ -440,9 +479,10 @@ private static final class Seat { /** Append an asteroid belt at {@code orbit}, if the neighbourhood still has a cell for one. */ private static void addBelt(List bodies, long seed, GalacticCoord anchor, int orbit, - StellarBody star, long s, int starId, Set taken, int index) { + StellarBody star, Lattice lattice, int starId, Set taken, + int index) { int clamped = Math.max(1, orbit); - Seat seat = seatBody(seed, anchor, index, clamped, star, s, taken); + Seat seat = seatBody(seed, anchor, index, clamped, star, lattice, taken); if (seat != null) { // A belt is centred on the star it rings, so as a whole it does not travel round it. Its // cell is a marker on the ring; the ring itself does not go anywhere. @@ -508,10 +548,8 @@ public BodyProfile profileOf(long seed, GalacticCoord anchor, SystemBody body, S @Override public Optional anchorAt(long seed, GalacticCoord cell) { - long s = config.minSpacing; - Optional g = systemForSuperCell(seed, - Math.floorDiv(cell.sectorX(), s), Math.floorDiv(cell.sectorY(), s), - Math.floorDiv(cell.sectorZ(), s)); + Optional g = systemForLattice(seed, + latticeAt(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ())); return g.isPresent() ? Optional.of(g.get().cell) : Optional.empty(); } @@ -520,64 +558,201 @@ public int minSpacingCells() { return config.minSpacing; } - /** Per-axis clamp of a body's cell into its anchor's super-cell box (margin when the box allows it). */ - private static GalacticCoord clampIntoSuperCell(GalacticCoord bodyCell, GalacticCoord anchor, long s) { - long margin = (s > 2L * NEIGHBOURHOOD_MARGIN_CELLS) ? NEIGHBOURHOOD_MARGIN_CELLS : 0L; - long cx = clampAxis(bodyCell.sectorX(), anchor.sectorX(), s, margin); - long cy = clampAxis(bodyCell.sectorY(), anchor.sectorY(), s, margin); - long cz = clampAxis(bodyCell.sectorZ(), anchor.sectorZ(), s, margin); + @Override + public Optional declarationOriginOf(long seed, GalaxyKey key) { + return galaxies.declarationOriginOf(seed, key); + } + + @Override + public double guaranteedAuthoredReachLy() { + return UniverseScale.GUARANTEED_AUTHORED_REACH_LY; + } + + /** + * Per-axis clamp of a body's cell into its anchor's own LATTICE cell (margin when the box allows + * it), so a system's neighbourhood cannot reach into a neighbour's however far an orbit runs. + * + *

Against the lattice cell rather than a spacing, because inside a star cluster the cell is a + * sub-cell whose bounds are not a multiple of its own edge — dividing to find the box would put + * the box somewhere else entirely.

+ */ + private static GalacticCoord clampIntoLattice(GalacticCoord bodyCell, Lattice lattice) { + long cx = clampAxis(bodyCell.sectorX(), lattice.lowX, lattice.edgeX); + long cy = clampAxis(bodyCell.sectorY(), lattice.lowY, lattice.edgeY); + long cz = clampAxis(bodyCell.sectorZ(), lattice.lowZ, lattice.edgeZ); if (cx == bodyCell.sectorX() && cy == bodyCell.sectorY() && cz == bodyCell.sectorZ()) { return bodyCell; } return GalacticCoord.ofSectorLocal(cx, cy, cz, 0L, 0L, 0L); } - private static long clampAxis(long sector, long anchorSector, long s, long margin) { - long sup = Math.floorDiv(anchorSector, s); - long lo = sup * s + margin; - long hi = sup * s + s - 1L - margin; + private static long clampAxis(long sector, long low, long edge) { + long margin = (edge > 2L * NEIGHBOURHOOD_MARGIN_CELLS) ? NEIGHBOURHOOD_MARGIN_CELLS : 0L; + long lo = low + margin; + long hi = low + edge - 1L - margin; if (sector < lo) { return lo; } return sector > hi ? hi : sector; } - /** The single system a super-cell hosts (its cell coordinate + fabricated system), or empty. */ - private Optional systemForSuperCell(long seed, long supX, long supY, long supZ) { - long s = config.minSpacing; + /** The single system a lattice cell hosts (its cell coordinate + fabricated system), or empty. */ + private Optional systemForLattice(long seed, Lattice lattice) { // OCCUPANCY IS DECIDED IN THE GALAXY'S OWN FRAME, so the profile does the drawing: the disc, // the bulge and the arms place the stars. An independent per-cell draw could only ever produce // a uniform fog, which is what made "which galaxy is this?" a question with no answer. // - // Evaluated at the super-cell's CENTRE — a point fixed by the partition, not by any draw, so + // Evaluated at the lattice cell's CENTRE — a point fixed by the partition, not by any draw, so // the probability a cube is occupied cannot depend on where its seat would have landed. And // evaluated at t = 0 and never again: a time-dependent occupancy would pop systems in and out // of existence. Systems drift afterwards at their galaxy's own omega(r), which is the shear. - double profile = galaxyProfileAt(seed, supX * s + s / 2L, supY * s + s / 2L, supZ * s + s / 2L); + double profile = galaxyProfileAt(seed, lattice.lowX + lattice.edgeX / 2L, + lattice.lowY + lattice.edgeY / 2L, lattice.lowZ + lattice.edgeZ / 2L); if (!(profile > 0d)) { return Optional.empty(); // intergalactic void, or past this galaxy's edge } - if (CellHash.norm(CellHash.of(seed, supX, supY, supZ, SALT_OCC)) >= config.density * profile) { + // Keyed by the cell's LOW CORNER, which is globally unique whatever lattice it belongs to — + // a coarse index would collide with a fine one wherever a cluster refines the field. + if (CellHash.norm(lattice.hash(seed, SALT_OCC)) >= Math.min(1d, config.density * profile)) { return Optional.empty(); } // Seat the anchor anywhere in its cube except a declared margin at the faces. That margin is // the system's own CLEAR SPACE, not a fraction of the cube: it is what guarantees two stars // never stand closer than the separation floor, and what keeps one system's named bodies from - // reaching into the next cube (so member-cell attribution by floorDiv stays exact). + // reaching into the next cube (so member-cell attribution stays exact). // // It used to be the middle quarter per axis, which confined the seat to 1.6 % of the cube's // volume — a lattice of tight clumps with guaranteed-empty walls between them, visible in any // rendered star field. The margin now costs a couple of percent per face instead, because it // is sized by what a system actually needs rather than by the distance to the next star. - long margin = UniverseScale.seatMarginCells(s); - long band = Math.max(1L, s - 2L * margin); - long base = margin; - long ox = base + Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_OX), band); - long oy = base + Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_OY), band); - long oz = base + Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_OZ), band); - GalacticCoord cell = GalacticCoord.ofSectorLocal(supX * s + ox, supY * s + oy, supZ * s + oz, - 0L, 0L, 0L); - return Optional.of(new Generated(cell, fabricate(seed, supX, supY, supZ))); + // + // It is read off the LOCAL edge, so inside a cluster the floor shrinks with the lattice: stars + // in a globular core really do stand closer than a wide binary, and a system there loses outer + // bodies by the same rule that has always applied. + GalacticCoord cell = GalacticCoord.ofSectorLocal( + lattice.lowX + seatOffset(seed, lattice, SALT_OX, lattice.edgeX), + lattice.lowY + seatOffset(seed, lattice, SALT_OY, lattice.edgeY), + lattice.lowZ + seatOffset(seed, lattice, SALT_OZ, lattice.edgeZ), 0L, 0L, 0L); + return Optional.of(new Generated(cell, fabricate(seed, lattice))); + } + + /** Where the seat sits on one axis of its lattice cell, clear of the faces by the local margin. */ + private static long seatOffset(long seed, Lattice lattice, long salt, long edge) { + long margin = UniverseScale.seatMarginCells(edge); + long band = Math.max(1L, edge - 2L * margin); + return margin + Math.floorMod(lattice.hash(seed, salt), band); + } + + /** + * One cell of the star lattice: a coarse super-cell, or one of the {@code k³} sub-cells a star + * cluster divides it into. + * + *

Its bounds are PROPORTIONED rather than divided, so the fine lattice tiles a coarse cell of + * any edge exactly — a plain {@code s / k} would leave a remainder at the top of every coarse + * cell, and a remainder is a seam.

+ */ + private static final class Lattice { + final long lowX; + final long lowY; + final long lowZ; + final long edgeX; + final long edgeY; + final long edgeZ; + + private Lattice(long lowX, long lowY, long lowZ, long edgeX, long edgeY, long edgeZ) { + this.lowX = lowX; + this.lowY = lowY; + this.lowZ = lowZ; + this.edgeX = edgeX; + this.edgeY = edgeY; + this.edgeZ = edgeZ; + } + + /** Sub-cell {@code (i, j, m)} of coarse super-cell {@code (supX, supY, supZ)}, at {@code k}. */ + static Lattice of(long supX, long supY, long supZ, long i, long j, long m, int k, long s) { + long baseX = supX * s; + long baseY = supY * s; + long baseZ = supZ * s; + long loI = Math.floorDiv(i * s, (long) k); + long loJ = Math.floorDiv(j * s, (long) k); + long loM = Math.floorDiv(m * s, (long) k); + return new Lattice(baseX + loI, baseY + loJ, baseZ + loM, + Math.max(1L, Math.floorDiv((i + 1L) * s, (long) k) - loI), + Math.max(1L, Math.floorDiv((j + 1L) * s, (long) k) - loJ), + Math.max(1L, Math.floorDiv((m + 1L) * s, (long) k) - loM)); + } + + /** Its draw for one field, keyed by the low corner — globally unique at any subdivision. */ + long hash(long seed, long salt) { + return CellHash.of(seed, lowX, lowY, lowZ, salt); + } + + /** Whether {@code sector} lies inside this cell on the axis whose low/edge are given. */ + static boolean within(long sector, long low, long edge) { + return sector >= low && sector < low + edge; + } + + boolean contains(long sectorX, long sectorY, long sectorZ) { + return within(sectorX, lowX, edgeX) && within(sectorY, lowY, edgeY) + && within(sectorZ, lowZ, edgeZ); + } + + /** + * The smallest of its three edges — what a system's room is measured against. The edges differ + * by at most one cell, and taking the smallest is what keeps a neighbourhood inside its cell on + * every axis rather than on the average of them. + */ + long minEdge() { + return Math.min(edgeX, Math.min(edgeY, edgeZ)); + } + } + + /** + * How finely the lattice is divided at this coarse super-cell: {@code 1} in the ordinary field, + * and the cluster's {@code k} where one covers it. + * + *

Membership is a property of the COARSE cell, which is what keeps this an O(1) question with + * one answer — and what makes the fine lattice tile the coarse cells it replaces exactly.

+ */ + private int subdivisionAt(long seed, long supX, long supY, long supZ) { + long s = config.minSpacing; + Optional galaxy = galaxies.galaxyOwningSector(seed, supX * s + s / 2L, + supY * s + s / 2L, supZ * s + s / 2L); + if (!galaxy.isPresent()) { + return 1; + } + Optional cluster = clusters.clusterAt(seed, galaxy.get(), supX, supY, supZ); + if (!cluster.isPresent()) { + return 1; + } + // A cluster cannot conjure room its coarse cell never had. Refining below the smallest cell a + // system can be more than a lone star in would not make a dense cluster — it would make a + // field of bare stars, which is the opposite of the thing. A spacing too tight to refine is a + // degenerate galaxy rather than an error, exactly as too tight a spacing already is. + long ceiling = Math.max(1L, s / UniverseScale.MIN_LATTICE_EDGE_CELLS); + return (int) Math.max(1L, Math.min(cluster.get().subdivision(), ceiling)); + } + + /** The lattice cell a sector triple falls in. */ + private Lattice latticeAt(long seed, long sectorX, long sectorY, long sectorZ) { + long s = config.minSpacing; + long supX = Math.floorDiv(sectorX, s); + long supY = Math.floorDiv(sectorY, s); + long supZ = Math.floorDiv(sectorZ, s); + int k = subdivisionAt(seed, supX, supY, supZ); + if (k <= 1) { + return Lattice.of(supX, supY, supZ, 0L, 0L, 0L, 1, s); + } + return Lattice.of(supX, supY, supZ, + subIndex(Math.floorMod(sectorX, s), s, k), + subIndex(Math.floorMod(sectorY, s), s, k), + subIndex(Math.floorMod(sectorZ, s), s, k), k, s); + } + + /** Which sub-cell an offset inside a coarse cell falls in, on one axis. */ + private static long subIndex(long offsetInCoarse, long coarseEdge, int k) { + long index = Math.floorDiv(offsetInCoarse * (long) k, Math.max(1L, coarseEdge)); + return Math.min((long) k - 1L, Math.max(0L, index)); } /** @@ -595,7 +770,10 @@ private double galaxyProfileAt(long seed, long sectorX, long sectorY, long secto return galaxy.get().densityAtSector(sectorX, sectorY, sectorZ); } - private StarSystem fabricate(long seed, long supX, long supY, long supZ) { + private StarSystem fabricate(long seed, Lattice lattice) { + long supX = lattice.lowX; + long supY = lattice.lowY; + long supZ = lattice.lowZ; GalaxyGenConfig.StarType type = pickType(CellHash.of(seed, supX, supY, supZ, SALT_TYPE)); double sizeFrac = CellHash.norm(CellHash.of(seed, supX, supY, supZ, SALT_SIZE)); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Cosmology.java b/src/main/java/zmaster587/advancedRocketry/universe/Cosmology.java new file mode 100644 index 000000000..57ee5b942 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/Cosmology.java @@ -0,0 +1,72 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; + +/** + * The one law above every galaxy: how much bigger the universe is at tick {@code t} than it was when + * the world was made. + * + *

{@code a(0) = 1} by definition — {@code t = 0} is world creation, so the universe's age is the + * save's age. That is not an approximation to something else; there is no other clock the universe + * layer could be measured against.

+ * + *

Expansion is MONOTONE, where rotation is not

+ *

A shear-separated target comes back: two systems at different galactic radii drift apart and then + * together again, because {@code theta} wraps. An expansion-separated one does not. {@code a(t)} only + * ever grows, so a galaxy that recedes past a drive's reach has receded permanently — and that is a + * stronger claim about a player's world than "the sky moves slowly", which is why it is written down + * here rather than left implicit in a formula.

+ * + *

Which clock

+ *

Everything here is per TICK through the ORBITAL CALENDAR: a year is + * {@link AstronomicalBodyHelper#DAYS_PER_YEAR} days because that is the period of a one-AU orbit about + * a one-solar-mass star, and every other rate in this layer is quoted against the same year. Reading a + * tick as a twentieth of a REAL second instead would put a planet's year and a galaxy's recession on + * two different clocks, and the two would disagree by a factor of 548.

+ * + *

Scale

+ *

The galaxy lattice is compressed against reality (see {@link UniverseScale}), and the Hubble + * constant is NOT compressed with it — it is the real one. The consequence is deliberate and physical: + * at 75 000 light years apart, neighbouring galaxies recede at about 1.6 km/s while their own peculiar + * velocities run in the hundreds. So this universe behaves like a bound GROUP, where peculiar motion + * dominates and expansion is the slow background — which is exactly what a real galaxy group does.

+ */ +public final class Cosmology { + + /** The Hubble constant in km/s per megaparsec — the measured one, uncompressed. */ + public static final double HUBBLE_KM_S_PER_MEGAPARSEC = 70d; + + /** Light years in one megaparsec — what carries the Hubble constant into this layer's unit. */ + public static final double LIGHT_YEARS_PER_MEGAPARSEC = 3_261_563.777d; + + /** + * The fractional rate at which every intergalactic separation grows, per tick. Derived, never + * written as a literal: it is the Hubble constant expressed in this layer's length and this + * layer's clock. + */ + public static final double HUBBLE_PER_TICK = + UniverseScale.lightYearsPerTick(HUBBLE_KM_S_PER_MEGAPARSEC) / LIGHT_YEARS_PER_MEGAPARSEC; + + /** + * The horizon a galaxy's drift is BOUNDED against, in ticks — about 870 years of world time, or a + * couple of real years of continuous play. + * + *

A galaxy that wandered out of its own lattice cell would break three things at once: + * at-most-one-galaxy-per-cell, non-overlap, and the O(1) answer to "which galaxy is this point in", + * which reads the containing cell and nothing else. So a drawn velocity is clamped to keep the + * galaxy inside its cell for at least this long. At realistic speeds the clamp is five orders away + * from binding, which is the point of measuring it rather than asserting it.

+ */ + public static final long DRIFT_HORIZON_TICKS = 1_000_000_000L; + + private Cosmology() { + } + + /** + * How much bigger the universe is at {@code tick} than at world creation. {@code a(0) = 1}, and it + * only ever grows. + */ + public static double scaleFactorAt(long tick) { + return Math.exp(HUBBLE_PER_TICK * (double) tick); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalacticAnchor.java b/src/main/java/zmaster587/advancedRocketry/universe/GalacticAnchor.java new file mode 100644 index 000000000..f2c5c6d73 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalacticAnchor.java @@ -0,0 +1,78 @@ +package zmaster587.advancedRocketry.universe; + +import java.util.Optional; + +import zmaster587.advancedRocketry.space.GalacticCoord; + +/** + * Where an AUTHORED system is declared to be: a {@link GalaxyKey} plus a cell offset from that + * galaxy's centre. + * + *

This is deliberately not a position. It is resolved into an absolute cell name ONCE, at + * {@code t = 0} — the reference angle — after which the system is named by that cell exactly like + * every other, and rotates with its galaxy exactly like a procedural one. So nothing downstream gains + * a second kind of address, and a coordinate a player wrote down keeps meaning what it meant.

+ * + *

Immutable value type.

+ */ +public final class GalacticAnchor { + + private final GalaxyKey galaxy; + private final GalacticCoord local; + + private GalacticAnchor(GalaxyKey galaxy, GalacticCoord local) { + this.galaxy = galaxy; + this.local = local; + } + + public static GalacticAnchor of(GalaxyKey galaxy, GalacticCoord local) { + return new GalacticAnchor(galaxy == null ? GalaxyKey.HOME : galaxy, + local == null ? GalacticCoord.ORIGIN : local); + } + + /** An anchor in the home galaxy — what an unqualified declaration means. */ + public static GalacticAnchor inHome(GalacticCoord local) { + return of(GalaxyKey.HOME, local); + } + + public GalaxyKey galaxy() { + return galaxy; + } + + /** The offset from the galaxy's centre, as a cell triple. */ + public GalacticCoord local() { + return local; + } + + /** + * The absolute cell this anchor denotes, given where its galaxy's centre actually is. + * + *

{@code centre} empty means the running generator has no galaxies at all — an authored-only + * universe. There the declaration IS the absolute cell, which is both the old behaviour and the + * only reading that can be right: with nothing to be local to, local and absolute coincide.

+ */ + public GalacticCoord resolve(Optional centre) { + if (!centre.isPresent()) { + return local; + } + GalacticCoord c = centre.get(); + return GalacticCoord.ofSectorLocal(c.sectorX() + local.sectorX(), + c.sectorY() + local.sectorY(), c.sectorZ() + local.sectorZ(), 0L, 0L, 0L); + } + + /** + * How far out this anchor sits from its galaxy's centre, in light years — what the guaranteed + * minimum radius is checked against. + */ + public double reachLy() { + double x = UniverseScale.lightYearsForCells(local.sectorX()); + double y = UniverseScale.lightYearsForCells(local.sectorY()); + double z = UniverseScale.lightYearsForCells(local.sectorZ()); + return Math.sqrt(x * x + y * y + z * z); + } + + @Override + public String toString() { + return "GalacticAnchor[" + galaxy + " + " + local.cellKey() + "]"; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalacticFrame.java b/src/main/java/zmaster587/advancedRocketry/universe/GalacticFrame.java new file mode 100644 index 000000000..f561de109 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalacticFrame.java @@ -0,0 +1,35 @@ +package zmaster587.advancedRocketry.universe; + +/** + * Which law carries a position through time — the intergalactic regime, in two states and no more. + * + *

There is no "nowhere". Every point belongs to exactly one galaxy CELL; a galaxy occupies a + * small sphere inside its cell and the rest of that cell is void. So no coordinate ever carries a null + * galaxy and no call site needs a branch for a point that is in no galaxy at all — only for a point + * that is in the void OF one.

+ * + *

The two states are physically different, not a convenience: matter bound to a galaxy co-rotates + * with it and does not expand, while matter in the void is carried by the Hubble flow. A craft parked + * in the void stays put relative to the void while the galaxies recede from it.

+ * + *

The frame is LATCHED at the crossing, never re-derived per tick

+ *

The boundary is a threshold, so anything hovering on it would flip frame every tick — and the + * frame decides both rotation and expansion. {@link GalaxyField#frameAt} answers the question ONCE, at + * a crossing; a moving craft stores the answer alongside the cell binding it already stores. Every + * position-keyed defect this tree has logged has the same shape: a decision re-derived from a + * coordinate instead of held as identity.

+ */ +public enum GalacticFrame { + + /** + * Bound to a galaxy: the position is an offset from the galaxy's CENTRE, it turns with the disc at + * {@code ω(r)}, and it does not expand. + */ + GALACTIC, + + /** + * Out in the void: the position is an offset from the galaxy CELL's origin and it is comoving — + * it scales with {@code a(t)} and does not rotate. + */ + COMOVING +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java index a7fbacf38..dd3acebed 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java @@ -45,11 +45,20 @@ public final class Galaxy { private static final double ARM_CONTRAST = 0.6d; /** The centre is a singular point of the arm winding; inside this fraction the bulge speaks. */ private static final double ARM_INNER_FRACTION = 1e-3d; + /** + * What the profile is divided by, so that a point ON AN ARM at the sun-like galactic radius scores + * 1. It is the disc term there, and it is scale-free — the exponentials are all in units of the + * radius, so this one number normalises a galaxy of any size. + */ + private static final double REFERENCE_LEVEL = + Math.exp(-UniverseScale.HOME_GALAXY_ORIGIN_FRACTION / DISC_SCALE_FRACTION); private final long cellX; private final long cellY; private final long cellZ; private final GalacticCoord centre; + private final LightYearVector seat; + private final LightYearVector peculiarVelocity; private final GalaxyGenConfig.GalaxyType type; private final double radiusLy; private final double armPitch; @@ -76,33 +85,61 @@ public final class Galaxy { * @param node the direction that pole leans in, in radians about +Y * @param armPitch the arms' pitch angle in radians (ignored when the type has no arms) * @param armPhase where arm zero starts, in radians + * @param peculiarVelocity its comoving velocity in light years per tick — its own motion through + * the expanding universe, on top of the expansion */ public Galaxy(long cellX, long cellY, long cellZ, GalacticCoord centre, GalaxyGenConfig.GalaxyType type, double radiusLy, double tilt, double node, - double armPitch, double armPhase) { + double armPitch, double armPhase, LightYearVector peculiarVelocity) { this.cellX = cellX; this.cellY = cellY; this.cellZ = cellZ; this.centre = centre; + this.seat = LightYearVector.ofCell(centre); + this.peculiarVelocity = (peculiarVelocity == null) ? LightYearVector.ZERO : peculiarVelocity; this.type = type; this.radiusLy = Math.max(1d, radiusLy); this.armPitch = armPitch; this.armPhase = armPhase; + double[] basis = basisOf(tilt, node); + this.ux = basis[0]; + this.uy = basis[1]; + this.uz = basis[2]; + this.vx = basis[3]; + this.vy = basis[4]; + this.vz = basis[5]; + this.wx = basis[6]; + this.wy = basis[7]; + this.wz = basis[8]; + } + + /** + * The orthonormal frame of a galaxy with this orientation: {@code u} and {@code v} span its plane, + * {@code w} is its pole. Laid out as {@code [ux,uy,uz, vx,vy,vz, wx,wy,wz]}. + */ + private static double[] basisOf(double tilt, double node) { double st = Math.sin(tilt); double ct = Math.cos(tilt); double sn = Math.sin(node); double cn = Math.cos(node); - // w = the pole; u, v = an orthonormal pair spanning the plane it is normal to. - this.wx = st * cn; - this.wy = ct; - this.wz = st * sn; - this.ux = ct * cn; - this.uy = -st; - this.uz = ct * sn; - this.vx = -sn; - this.vy = 0d; - this.vz = cn; + return new double[] { + ct * cn, -st, ct * sn, + -sn, 0d, cn, + st * cn, ct, st * sn, + }; + } + + /** + * A unit vector lying IN the plane of a galaxy with this orientation, at in-plane angle + * {@code angle}. What a caller uses to put something at a stated galactic radius in the DISC, + * rather than somewhere in the halo above it. + */ + public static LightYearVector planeDirection(double tilt, double node, double angle) { + double[] b = basisOf(tilt, node); + double c = Math.cos(angle); + double s = Math.sin(angle); + return LightYearVector.of(c * b[0] + s * b[3], c * b[1] + s * b[4], c * b[2] + s * b[5]); } public long cellX() { @@ -117,11 +154,19 @@ public long cellZ() { return cellZ; } - /** Where this galaxy's centre stands, as a cell name. */ + /** + * Where this galaxy's centre stands, as a cell NAME — the seat it was drawn at, at {@code t = 0}. + * A name is not a place: for where the centre actually is at a tick, see {@link #centreAt}. + */ public GalacticCoord centre() { return centre; } + /** Its comoving velocity, in light years per tick — its own motion, on top of the expansion. */ + public LightYearVector peculiarVelocity() { + return peculiarVelocity; + } + public GalaxyGenConfig.GalaxyType type() { return type; } @@ -162,8 +207,17 @@ public boolean containsSector(long sectorX, long sectorY, long sectorZ) { } /** - * How dense this galaxy is at a point {@code (dx, dy, dz)} light years from its centre, as a - * fraction of its densest point: {@code 0} outside the radius, {@code 1} at the nucleus. + * How dense this galaxy is at a point {@code (dx, dy, dz)} light years from its centre, relative to + * a SUN-LIKE spot in its disc: {@code 0} outside the radius, about {@code 1} out where the home + * galaxy puts the origin, and several times that in the nucleus. + * + *

Normalised at the sun-like radius, not at the nucleus, and that choice is load-bearing. + * The mean star separation is the primary quantity of this whole layer and it is REAL — it is the + * separation in the solar neighbourhood. So the configured density has to mean "how full a sky + * like ours is"; normalising at the nucleus instead would have made every configured density a + * statement about the galactic core, and left the sky a player actually stands under five times + * too empty. The centre goes above 1 and is clamped where the probability is used, which is the + * honest place for a saturation.

* *

This is the ONE function that decides both where stars are placed and what shape a galaxy * reads as. A disc is an exponential disc times an exponential in height, modulated by arms and @@ -184,7 +238,7 @@ public double densityAt(double dxLy, double dyLy, double dzLy) { // Round, with the type's flattening squashing the pole. No plane, so no arms and no bulge // term — the whole thing IS the bulge. double scaled = Math.hypot(r, z / Math.max(1e-6d, type.scaleHeightRatio)); - return clamp01(Math.exp(-scaled / (radiusLy * DISC_SCALE_FRACTION))); + return atLeastZero(Math.exp(-scaled / (radiusLy * DISC_SCALE_FRACTION)) / REFERENCE_LEVEL); } double scaleHeight = Math.max(1e-6d, radiusLy * type.scaleHeightRatio); @@ -192,7 +246,7 @@ public double densityAt(double dxLy, double dyLy, double dzLy) { * Math.exp(-Math.abs(z) / scaleHeight); disc *= armFactor(r, Math.atan2(localY, localX)); double bulge = Math.exp(-Math.hypot(r, z) / (radiusLy * BULGE_SCALE_FRACTION)); - return clamp01(disc + bulge); + return atLeastZero((disc + bulge) / REFERENCE_LEVEL); } /** The profile read at a cell name — the form the generator asks in. */ @@ -251,6 +305,72 @@ public double rotationPeriodTicks(double rLy) { return omega > 0d ? 2d * Math.PI / omega : Double.POSITIVE_INFINITY; } + // ─── Where the galaxy itself is ──────────────────────────────────────────── + + /** + * Where this galaxy's centre stands at tick {@code t}: {@code C(t) = a(t) · (C₀ + v·t)}. + * + *

Expansion carries the centre and nothing inside the galaxy. A gravitationally bound + * system does not expand, and scaling intra-galactic coordinates would grow every {@code r} and + * corrupt {@code ω(r)} from within — so the split is structural rather than a rule someone has to + * remember: everything below is written as an offset from this point.

+ * + *

Expansion alone would let a galaxy only ever RECEDE, which makes an approaching neighbour + * unrepresentable — and in a real group at short range peculiar motion dominates expansion. Hence + * the velocity term, one hash draw, still analytic, still nothing integrated.

+ */ + public LightYearVector centreAt(long tick) { + return seat.plus(peculiarVelocity.scale((double) tick)) + .scale(Cosmology.scaleFactorAt(tick)); + } + + /** + * Where a point BOUND to this galaxy stands at tick {@code t}, absolutely. + * + *

It rides the galaxy: it turns with the disc at {@code ω(r)} and it does not expand. The + * arguments are its galaxy-local cylindrical elements at {@code t = 0}, which are what a bound + * thing actually has — a radius, an angle and a height, exactly as a planet has an orbit.

+ */ + public LightYearVector boundPositionAt(long tick, double rLy, double theta0, double heightLy) { + double theta = thetaAt(theta0, rLy, tick); + double localX = rLy * Math.cos(theta); + double localY = rLy * Math.sin(theta); + // Back out of the galaxy frame: the basis is orthonormal, so the inverse is its transpose. + return centreAt(tick).plus(LightYearVector.of( + localX * ux + localY * vx + heightLy * wx, + localX * uy + localY * vy + heightLy * wy, + localX * uz + localY * vz + heightLy * wz)); + } + + /** The galaxy-local radius of a static-frame offset from the centre, in light years. */ + public double localRadius(double dxLy, double dyLy, double dzLy) { + return Math.hypot(dxLy * ux + dyLy * uy + dzLy * uz, dxLy * vx + dyLy * vy + dzLy * vz); + } + + /** The galaxy-local angle of a static-frame offset from the centre, in radians. */ + public double localTheta(double dxLy, double dyLy, double dzLy) { + return Math.atan2(dxLy * vx + dyLy * vy + dzLy * vz, dxLy * ux + dyLy * uy + dzLy * uz); + } + + /** The height of a static-frame offset above this galaxy's plane, in light years. */ + public double localHeight(double dxLy, double dyLy, double dzLy) { + return dxLy * wx + dyLy * wy + dzLy * wz; + } + + /** + * Where the cell named {@code cell} stands at tick {@code t}, IF it is bound to this galaxy. + * + *

Its elements are read once, off its offset from the seat at {@code t = 0} — that is what a + * cell NAME means here, and it is why a name stays put while the place it names moves.

+ */ + public LightYearVector boundPositionOfCellAt(GalacticCoord cell, long tick) { + double dx = offsetLy(cell.sectorX(), centre.sectorX()); + double dy = offsetLy(cell.sectorY(), centre.sectorY()); + double dz = offsetLy(cell.sectorZ(), centre.sectorZ()); + return boundPositionAt(tick, localRadius(dx, dy, dz), localTheta(dx, dy, dz), + localHeight(dx, dy, dz)); + } + // ─── Helpers ─────────────────────────────────────────────────────────────── /** A sector delta as a length in light years. Exact: the delta is bounded by one galaxy cell. */ @@ -258,11 +378,8 @@ private static double offsetLy(long sector, long centreSector) { return UniverseScale.lightYearsForCells((double) (sector - centreSector)); } - private static double clamp01(double v) { - if (!(v > 0d)) { - return 0d; - } - return v > 1d ? 1d : v; + private static double atLeastZero(double v) { + return v > 0d ? v : 0d; } @Override diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java index 298f20f38..d9fcf1c45 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java @@ -25,12 +25,16 @@ * cell's galaxy, {@link Galaxy#containsSector} says whether you are in it.

* *

The home galaxy

- *

Galaxy cell {@code (0,0,0)} is RESERVED: it always holds a galaxy, centred on the origin, drawn - * only among types large enough to hold authored content. A galaxy is otherwise a hash draw and may - * simply not be there under another seed — but authored content must exist under EVERY seed, and a - * hand-picked absolute coordinate would otherwise land in intergalactic space with probability - * 99.997 %. Only its EXISTENCE and its centre are fixed; its type, size, orientation and arms are - * drawn like any other galaxy's, so every world's home galaxy is still its own.

+ *

Galaxy cell {@code (0,0,0)} is RESERVED: it always holds a galaxy, seated so that the universe + * ORIGIN falls at a sun-like radius inside its disc, and drawn only among types large enough to hold + * authored content. A galaxy is otherwise a hash draw and may simply not be there under another seed — + * but authored content must exist under EVERY seed, and a hand-picked absolute coordinate would + * otherwise land in intergalactic space with probability 99.997 %.

+ * + *

Around the origin, not ON it. The centre of a galaxy is its nucleus, which is the last + * address a shipped solar system should have. Only the galaxy's EXISTENCE and the origin's place + * inside it are fixed; its type, size, orientation and arms are drawn like any other galaxy's, so + * every world's home galaxy is still its own.

*/ public final class GalaxyField { @@ -46,11 +50,22 @@ public final class GalaxyField { private static final long SALT_GALAXY_NODE = 0x108L; private static final long SALT_GALAXY_PITCH = 0x109L; private static final long SALT_GALAXY_PHASE = 0x10AL; + private static final long SALT_GALAXY_SPEED = 0x10BL; + private static final long SALT_GALAXY_HEADING = 0x10CL; + private static final long SALT_GALAXY_ELEVATION = 0x10DL; + private static final long SALT_GALAXY_HOME_ANGLE = 0x10EL; /** Arms are drawn in this pitch band, in degrees — the range real spirals occupy. */ private static final double MIN_ARM_PITCH_DEGREES = 10d; private static final double MAX_ARM_PITCH_DEGREES = 30d; + /** + * A galaxy's own motion through the expanding universe, in km/s — the band real peculiar + * velocities occupy. Andromeda's 110 km/s sits inside it. + */ + private static final double MIN_PECULIAR_SPEED_KM_S = 50d; + private static final double MAX_PECULIAR_SPEED_KM_S = 600d; + private final GalaxyGenConfig config; private final long totalGalaxyWeight; private final long totalHomeWeight; @@ -61,7 +76,7 @@ public GalaxyField(GalaxyGenConfig config) { long home = 0L; for (GalaxyGenConfig.GalaxyType t : this.config.galaxyTypes) { all += t.weight; - if (qualifiesAsHome(t)) { + if (qualifiesForAuthoredContent(t)) { home += t.weight; } } @@ -128,6 +143,44 @@ public static boolean isHomeCell(long gx, long gy, long gz) { return gx == 0L && gy == 0L && gz == 0L; } + /** + * Whether this cell holds a galaxy WHATEVER the hash says — the home cell, or any key authored + * content was declared against. + */ + public boolean isReserved(long gx, long gy, long gz) { + for (GalaxyKey key : config.reservedGalaxies) { + if (key.gx() == gx && key.gy() == gy && key.gz() == gz) { + return true; + } + } + return false; + } + + /** + * The cell an authored anchor declared against {@code key} is measured FROM, or empty when that + * cell holds no galaxy. A reserved key always answers, which is the whole point of reserving it. + * + *

For {@code home} it is the universe ORIGIN, not the galaxy's centre — the home galaxy is + * seated around the origin rather than on it, and the origin is where authored content has always + * been declared. So a coordinate written before galaxies existed still means exactly what it did, + * and the galaxy is what moved to contain it.

+ */ + public Optional declarationOriginOf(long seed, GalaxyKey key) { + if (key != null && key.isHome()) { + return Optional.of(GalacticCoord.ORIGIN); + } + return centreOf(seed, key); + } + + /** Where the galaxy named by {@code key} is CENTRED, or empty when that cell holds no galaxy. */ + public Optional centreOf(long seed, GalaxyKey key) { + if (key == null) { + return Optional.empty(); + } + Optional galaxy = galaxyAtIndex(seed, key.gx(), key.gy(), key.gz()); + return galaxy.isPresent() ? Optional.of(galaxy.get().centre()) : Optional.empty(); + } + /** * The galaxy seated in galaxy cell {@code (gx, gy, gz)}, or empty when the cell is void. * @@ -136,11 +189,14 @@ public static boolean isHomeCell(long gx, long gy, long gz) { */ public Optional galaxyAtIndex(long seed, long gx, long gy, long gz) { boolean home = isHomeCell(gx, gy, gz); - if (!home && !occupied(seed, gx, gy, gz)) { + boolean reserved = home || isReserved(gx, gy, gz); + if (!reserved && !occupied(seed, gx, gy, gz)) { return Optional.empty(); } + // A reserved cell holds authored content, so its galaxy must be large enough to have room for + // it — the guarantee is a constraint on the TYPE DRAW, never a clamp applied to its result. GalaxyGenConfig.GalaxyType type = pickType(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_TYPE), - home); + reserved); double radiusFraction = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_RADIUS)); double radiusLy = type.minRadiusLy + radiusFraction * (type.maxRadiusLy - type.minRadiusLy); @@ -152,8 +208,87 @@ public Optional galaxyAtIndex(long seed, long gx, long gy, long gz) { * (MAX_ARM_PITCH_DEGREES - MIN_ARM_PITCH_DEGREES)); double phase = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_PHASE)) * 2d * Math.PI; - return Optional.of(new Galaxy(gx, gy, gz, seatOf(seed, gx, gy, gz, radiusLy, home), type, - radiusLy, tilt, node, pitch, phase)); + return Optional.of(new Galaxy(gx, gy, gz, + seatOf(seed, gx, gy, gz, radiusLy, tilt, node, home), type, + radiusLy, tilt, node, pitch, phase, + peculiarVelocityOf(seed, gx, gy, gz, radiusLy, home))); + } + + /** + * A galaxy's own motion through the expanding universe, in light years per tick. + * + *

The home galaxy has none. It is the rest frame authored content is declared in: if it + * drifted, the shipped solar system — named by absolute cells at {@code t = 0} — would be left + * behind by its own galaxy. Every other galaxy moves relative to it, which is also what an + * observer actually sees.

+ * + *

The speed is CLAMPED so the galaxy cannot leave its own lattice cell within + * {@link Cosmology#DRIFT_HORIZON_TICKS}. At realistic speeds the clamp is five orders from + * binding — so galaxy mergers are excluded by construction and nothing else is.

+ */ + private LightYearVector peculiarVelocityOf(long seed, long gx, long gy, long gz, double radiusLy, + boolean home) { + if (home) { + return LightYearVector.ZERO; + } + double u = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_SPEED)); + double kmPerSecond = MIN_PECULIAR_SPEED_KM_S + + u * (MAX_PECULIAR_SPEED_KM_S - MIN_PECULIAR_SPEED_KM_S); + double speed = Math.min(UniverseScale.lightYearsPerTick(kmPerSecond), + driftBudgetLy(radiusLy) / (double) Cosmology.DRIFT_HORIZON_TICKS); + + // Isotropic: cos(elevation) uniform, not the elevation itself, or the draws would pile up at + // the poles of whatever axis happened to be written first. + double heading = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_HEADING)) * 2d * Math.PI; + double cosEl = 2d * CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_ELEVATION)) - 1d; + double sinEl = Math.sqrt(Math.max(0d, 1d - cosEl * cosEl)); + return LightYearVector.of(speed * sinEl * Math.cos(heading), speed * cosEl, + speed * sinEl * Math.sin(heading)); + } + + /** How far a galaxy of this radius may drift before it would touch its own cell's face. */ + private double driftBudgetLy(double radiusLy) { + double halfCellLy = UniverseScale.lightYearsForCells(config.galaxySpacing / 2d); + return Math.max(0d, halfCellLy - radiusLy); + } + + // ─── The intergalactic regime ────────────────────────────────────────────── + + /** + * Which law carries this cell through time: bound to its galaxy, or comoving out in the void. + * + *

Ask this at a CROSSING and store the answer — see {@link GalacticFrame}. Calling it every + * tick for a moving craft is the frame-flapping this design exists to prevent.

+ */ + public GalacticFrame frameAt(long seed, GalacticCoord cell) { + Optional galaxy = galaxyOwning(seed, cell); + boolean bound = galaxy.isPresent() + && galaxy.get().containsSector(cell.sectorX(), cell.sectorY(), cell.sectorZ()); + return bound ? GalacticFrame.GALACTIC : GalacticFrame.COMOVING; + } + + /** + * Where the cell named {@code cell} actually is at tick {@code tick}, under whichever law governs + * it. The two laws meet here and nowhere else. + */ + public LightYearVector positionAt(long seed, GalacticCoord cell, long tick) { + Optional galaxy = galaxyOwning(seed, cell); + if (galaxy.isPresent() + && galaxy.get().containsSector(cell.sectorX(), cell.sectorY(), cell.sectorZ())) { + return galaxy.get().boundPositionOfCellAt(cell, tick); + } + return comovingPositionAt(cell, tick); + } + + /** + * Where a VOID cell is at tick {@code tick}: carried by the Hubble flow and nothing else. + * + *

Precision out here is identical to precision inside a galaxy, because the offset is measured + * from the cell's origin in the same light-year vocabulary — which is what choosing a galaxy scale + * whose cell fits one {@code long} of blocks bought.

+ */ + public static LightYearVector comovingPositionAt(GalacticCoord cell, long tick) { + return LightYearVector.ofCell(cell).scale(Cosmology.scaleFactorAt(tick)); } /** @@ -187,13 +322,21 @@ static double webDensity(long gx, long gy, long gz) { * galaxies that cannot overlap, and an O(1) answer to "which galaxy is this point in" that reads * the containing cell and nothing else.

* - *

The home galaxy is centred on the ORIGIN instead. Authored anchors are declared in absolute - * coordinates today, so the origin is where authored content actually is; seating the home galaxy - * anywhere else would put the shipped solar system in intergalactic space.

+ *

The home galaxy is seated AROUND the origin instead — the origin is where authored content + * is, so the galaxy has to contain it. Not ON it: the centre of a galaxy is its nucleus, and that + * is the last address a shipped solar system should have. The offset puts the origin at a + * sun-like galactic radius, in the plane, out in the disc.

*/ - private GalacticCoord seatOf(long seed, long gx, long gy, long gz, double radiusLy, boolean home) { + private GalacticCoord seatOf(long seed, long gx, long gy, long gz, double radiusLy, double tilt, + double node, boolean home) { if (home) { - return GalacticCoord.ORIGIN; + double angle = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_HOME_ANGLE)) + * 2d * Math.PI; + LightYearVector offset = Galaxy.planeDirection(tilt, node, angle) + .scale(-UniverseScale.HOME_GALAXY_ORIGIN_FRACTION * radiusLy); + return GalacticCoord.ofSectorLocal(UniverseScale.cellsAt(offset.x()), + UniverseScale.cellsAt(offset.y()), UniverseScale.cellsAt(offset.z()), + 0L, 0L, 0L); } long s = config.galaxySpacing; long margin = Math.min(UniverseScale.cellsForLightYears(radiusLy), Math.max(0L, (s - 1L) / 2L)); @@ -208,27 +351,28 @@ private GalacticCoord seatOf(long seed, long gx, long gy, long gz, double radius } /** - * Whether a type may be drawn for the HOME galaxy: its smallest possible radius must already - * clear the guaranteed minimum, so the guarantee is a constraint on the DRAW rather than a clamp - * applied to its result. + * Whether a type may be drawn for a galaxy that HOLDS AUTHORED CONTENT: its smallest possible + * radius must already clear the guaranteed minimum, so the guarantee is a constraint on the DRAW + * rather than a clamp applied to its result. */ - private static boolean qualifiesAsHome(GalaxyGenConfig.GalaxyType type) { - return type.minRadiusLy >= UniverseScale.MIN_HOME_GALAXY_RADIUS_LY; + private static boolean qualifiesForAuthoredContent(GalaxyGenConfig.GalaxyType type) { + return type.minRadiusLy >= UniverseScale.MIN_AUTHORED_GALAXY_RADIUS_LY; } /** - * Draw a type by weight — over the whole table, or over the subset a home galaxy may be. + * Draw a type by weight — over the whole table, or over the subset a galaxy holding authored + * content may be. * - *

A table with nothing large enough to be a home falls back to the whole table: a pack that - * ships only dwarf galaxies gets the universe it asked for, and its authored content had better - * be close to the centre.

+ *

A table with nothing large enough falls back to the whole table: a pack that ships only dwarf + * galaxies gets the universe it asked for, and its authored content had better be near the + * centre.

*/ - private GalaxyGenConfig.GalaxyType pickType(long h, boolean home) { - boolean restricted = home && totalHomeWeight > 0L; + private GalaxyGenConfig.GalaxyType pickType(long h, boolean restrictToLarge) { + boolean restricted = restrictToLarge && totalHomeWeight > 0L; long r = Math.floorMod(h, restricted ? totalHomeWeight : totalGalaxyWeight); GalaxyGenConfig.GalaxyType last = null; for (GalaxyGenConfig.GalaxyType t : config.galaxyTypes) { - if (restricted && !qualifiesAsHome(t)) { + if (restricted && !qualifiesForAuthoredContent(t)) { continue; } last = t; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index 446fc97e0..af47c0638 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -122,6 +122,38 @@ public GalaxyType(String name, GalaxyProfile profile, double minRadiusLy, double } } + /** + * A weighted STAR-CLUSTER archetype — the same seat one level DOWN, and the third table of the + * same shape. + * + *

The stratified lattice reads correctly as randomness but produces no GROUPS, and groups are + * what a real sky has: the lattice caps density at roughly three times the mean, while an open + * cluster runs tens of times the field. A cluster is therefore a seated object like a galaxy and + * like a system, and inside it the star lattice is finer.

+ * + *

{@code subdivision} is what makes this cheap rather than a graded spacing. The fine + * lattice divides each coarse super-cell into {@code k³} parts, so it tiles the coarse cells it + * replaces exactly — there is no boundary pathology and nothing has to be re-proved per ring. + * Density inside a cluster is {@code k³} times the field.

+ */ + public static final class ClusterType { + public final String name; + /** {@code k}: how many parts each coarse super-cell is divided into, per axis. */ + public final int subdivision; + public final double minRadiusLy; + public final double maxRadiusLy; + public final int weight; + + public ClusterType(String name, int subdivision, double minRadiusLy, double maxRadiusLy, + int weight) { + this.name = (name == null || name.isEmpty()) ? "CLUSTER" : name; + this.subdivision = Math.max(1, subdivision); + this.minRadiusLy = Math.max(0.01d, minRadiusLy); + this.maxRadiusLy = Math.max(this.minRadiusLy, maxRadiusLy); + this.weight = Math.max(1, weight); + } + } + /** * Per-super-cell occupancy probability, before the owning galaxy's profile scales it. It is the * density AT A GALAXY'S DENSEST POINT, not an average over space: outside a galaxy the profile is @@ -141,6 +173,13 @@ public GalaxyType(String name, GalaxyProfile profile, double minRadiusLy, double public final List starTypes; /** Galaxy archetypes sampled by weight when a galaxy is seated (never empty). */ public final List galaxyTypes; + /** Star-cluster archetypes sampled by weight when a cluster is seated (never empty). */ + public final List clusterTypes; + /** + * Galaxy cells that hold a galaxy WHATEVER the hash says — every key authored content is declared + * against. Always contains {@link GalaxyKey#HOME}: a pack that names no galaxy still has one. + */ + public final List reservedGalaxies; /** * Each lattice states its EDGE and then its OCCUPANCY, stars first and galaxies second, so the two @@ -148,6 +187,12 @@ public GalaxyType(String name, GalaxyProfile profile, double minRadiusLy, double */ public GalaxyGenConfig(int minSpacing, double density, long galaxySpacing, double galaxyDensity, List starTypes, List galaxyTypes) { + this(minSpacing, density, galaxySpacing, galaxyDensity, starTypes, galaxyTypes, null); + } + + public GalaxyGenConfig(int minSpacing, double density, long galaxySpacing, double galaxyDensity, + List starTypes, List galaxyTypes, + List reservedGalaxies) { this.density = clamp01(density); this.minSpacing = Math.max(1, minSpacing); this.galaxySpacing = Math.max(1L, galaxySpacing); @@ -158,6 +203,27 @@ public GalaxyGenConfig(int minSpacing, double density, long galaxySpacing, doubl this.galaxyTypes = (galaxyTypes == null || galaxyTypes.isEmpty()) ? defaultGalaxyTypes() : Collections.unmodifiableList(new ArrayList<>(galaxyTypes)); + this.clusterTypes = defaultClusterTypes(); + List reserved = new ArrayList<>(); + reserved.add(GalaxyKey.HOME); + if (reservedGalaxies != null) { + for (GalaxyKey key : reservedGalaxies) { + if (key != null && !reserved.contains(key)) { + reserved.add(key); + } + } + } + this.reservedGalaxies = Collections.unmodifiableList(reserved); + } + + /** + * The same configuration, reserving these galaxy cells as well. Authored anchors are discovered + * while the catalogue is walked, which is after {@code } has been read — so the keys + * they name are folded in here rather than parsed twice. + */ + public GalaxyGenConfig withReservedGalaxies(List keys) { + return new GalaxyGenConfig(minSpacing, density, galaxySpacing, galaxyDensity, starTypes, + galaxyTypes, keys); } /** A sparse, strongly-clustered default galaxy. */ @@ -191,6 +257,38 @@ private static List defaultGalaxyTypes() { return Collections.unmodifiableList(l); } + /** + * The stock cluster table. + * + *

The subdivisions are NOT the real-galaxy ones, and the reason is the scale choice one + * level up. A real nucleus runs about 10⁷ times the field density, i.e. {@code k = 215}. That + * number belongs to a galaxy of 10¹¹ stars; ours is compressed in RADIUS while the star separation + * stays real, so it holds of the order of a million — and a 5-light-year nucleus at {@code k = 215} + * would hold ninety times its own galaxy's entire population. {@code k = 25} puts a nucleus at + * about a tenth of its galaxy, which is what a real nuclear bulge is. Star separation is the + * primary quantity here and the galaxy accommodates it; the contrast has to follow that choice + * rather than be imported from the uncompressed world.

+ */ + private static List defaultClusterTypes() { + List l = new ArrayList<>(); + // name k radius band (ly) weight + l.add(new ClusterType("Open Cluster", 4, 5d, 15d, 80)); + l.add(new ClusterType("Globular Cluster", 14, 20d, 40d, 20)); + return Collections.unmodifiableList(l); + } + + /** + * The cluster every galaxy has at its own centre — the richest one, and no special case: it is a + * cluster like the others, drawn at the galaxy's centre instead of on the cluster lattice. + */ + public static final ClusterType NUCLEUS = new ClusterType("Nucleus", 25, 4d, 8d, 1); + + /** Edge of the cube that holds at most one cluster, in light years. */ + public static final double CLUSTER_SPACING_LY = 300d; + + /** Fraction of those cubes that hold a cluster, before the galaxy's own profile scales it. */ + public static final double CLUSTER_DENSITY = 0.35d; + private static double clamp01(double v) { if (Double.isNaN(v) || v < 0d) { return 0d; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyKey.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyKey.java new file mode 100644 index 000000000..a86e71975 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyKey.java @@ -0,0 +1,106 @@ +package zmaster587.advancedRocketry.universe; + +/** + * The name of one galaxy: its lattice index, or the reserved word {@code home}. + * + *

Authored content is declared against a key rather than at an absolute coordinate, and the reason + * is arithmetic: a galaxy fills about three thousandths of a percent of its own lattice cell, so a + * hand-picked absolute coordinate lands in intergalactic space with probability 99.997 %. Declaring + * {@code (galaxy, position within it)} is what makes an authored system land in a galaxy on every + * seed — and what lets it then rotate with that galaxy exactly like a procedural one, which an + * absolute declaration could never do.

+ * + *

A declared key FORCES its cell to hold a galaxy. A galaxy is otherwise a hash draw and may + * simply not be there under another seed, while authored content must exist under every seed. The + * key's parameters — type, radius, orientation, arms — stay hash-drawn, so only EXISTENCE is + * guaranteed and every world's galaxies are still its own.

+ * + *

Immutable value type.

+ */ +public final class GalaxyKey { + + /** The word a pack writes for the galaxy authored content lives in by default. */ + public static final String HOME_NAME = "home"; + + /** The reserved home galaxy: lattice cell (0,0,0), centred on the universe origin. */ + public static final GalaxyKey HOME = new GalaxyKey(0L, 0L, 0L); + + private final long gx; + private final long gy; + private final long gz; + + private GalaxyKey(long gx, long gy, long gz) { + this.gx = gx; + this.gy = gy; + this.gz = gz; + } + + public static GalaxyKey of(long gx, long gy, long gz) { + return new GalaxyKey(gx, gy, gz); + } + + /** + * Parse {@code "home"} or {@code "gx,gy,gz"}. Returns {@code null} for anything else — a malformed + * key is a thing the caller must report, not a thing this type may guess at. + */ + public static GalaxyKey parse(String text) { + if (text == null) { + return null; + } + String trimmed = text.trim(); + if (trimmed.isEmpty() || HOME_NAME.equalsIgnoreCase(trimmed)) { + return HOME; + } + String[] parts = trimmed.split(","); + if (parts.length != 3) { + return null; + } + try { + return new GalaxyKey(Long.parseLong(parts[0].trim()), Long.parseLong(parts[1].trim()), + Long.parseLong(parts[2].trim())); + } catch (NumberFormatException bad) { + return null; + } + } + + public long gx() { + return gx; + } + + public long gy() { + return gy; + } + + public long gz() { + return gz; + } + + /** Whether this is the home galaxy — the one centred on the origin. */ + public boolean isHome() { + return gx == 0L && gy == 0L && gz == 0L; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GalaxyKey)) { + return false; + } + GalaxyKey other = (GalaxyKey) o; + return gx == other.gx && gy == other.gy && gz == other.gz; + } + + @Override + public int hashCode() { + int result = Long.hashCode(gx); + result = 31 * result + Long.hashCode(gy); + return 31 * result + Long.hashCode(gz); + } + + @Override + public String toString() { + return isHome() ? HOME_NAME : (gx + "," + gy + "," + gz); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java index 91f7638c5..9f3992623 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java @@ -66,4 +66,25 @@ default Optional anchorAt(long seed, GalacticCoord cell) { default int minSpacingCells() { return GalaxyGenConfig.DEFAULT_MIN_SPACING; } + + /** + * The cell an authored anchor declared against {@code key} is measured FROM, or empty when this + * generator has no galaxies. + * + *

What an authored {@link GalacticAnchor} is resolved against. The default is empty, and that + * is the right answer rather than a stub: a generator with no galaxy tier has nothing for a + * declaration to be LOCAL to, so a declared position is already absolute — which is exactly the + * behaviour an authored-only universe had before galaxies existed.

+ */ + default Optional declarationOriginOf(long seed, GalaxyKey key) { + return Optional.empty(); + } + + /** + * How far from its DECLARATION ORIGIN authored content is guaranteed to stay inside its galaxy, + * in light years, or {@code 0} when this generator has no galaxies (and therefore no wall). + */ + default double guaranteedAuthoredReachLy() { + return 0d; + } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/LightYearVector.java b/src/main/java/zmaster587/advancedRocketry/universe/LightYearVector.java new file mode 100644 index 000000000..9db615d00 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/LightYearVector.java @@ -0,0 +1,83 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.space.GalacticCoord; + +/** + * A position or displacement in LIGHT YEARS — the vocabulary the galaxy layer is written in. + * + *

Why not blocks: an intergalactic position reaches 10¹² light years, which is 4·10²⁵ blocks and + * does not fit a {@code long}. Below the galaxy layer, blocks and a sectorised {@link GalacticCoord} + * are exactly right and stay so; above it, the honest type is a physical length in a {@code double}, + * whose relative precision is uniform at any magnitude. The conversion between the two lives in + * {@link UniverseScale} and nowhere else.

+ * + *

Immutable value type.

+ */ +public final class LightYearVector { + + public static final LightYearVector ZERO = new LightYearVector(0d, 0d, 0d); + + private final double x; + private final double y; + private final double z; + + private LightYearVector(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public static LightYearVector of(double x, double y, double z) { + return new LightYearVector(x, y, z); + } + + /** The position a cell NAME stands at in the static frame, in light years. */ + public static LightYearVector ofCell(GalacticCoord cell) { + return new LightYearVector( + UniverseScale.lightYearsForCells(cell.sectorX()), + UniverseScale.lightYearsForCells(cell.sectorY()), + UniverseScale.lightYearsForCells(cell.sectorZ())); + } + + public double x() { + return x; + } + + public double y() { + return y; + } + + public double z() { + return z; + } + + public LightYearVector plus(LightYearVector other) { + return new LightYearVector(x + other.x, y + other.y, z + other.z); + } + + public LightYearVector minus(LightYearVector other) { + return new LightYearVector(x - other.x, y - other.y, z - other.z); + } + + public LightYearVector scale(double factor) { + return new LightYearVector(x * factor, y * factor, z * factor); + } + + /** Length in light years. */ + public double length() { + return Math.sqrt(x * x + y * y + z * z); + } + + /** Distance to {@code other} in light years. */ + public double distanceTo(LightYearVector other) { + double dx = other.x - x; + double dy = other.y - y; + double dz = other.z - z; + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + @Override + public String toString() { + return "(" + x + ", " + y + ", " + z + ") ly"; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/StarCluster.java b/src/main/java/zmaster587/advancedRocketry/universe/StarCluster.java new file mode 100644 index 000000000..fe592ec72 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/StarCluster.java @@ -0,0 +1,117 @@ +package zmaster587.advancedRocketry.universe; + +/** + * One star cluster: a region of a galaxy where the star lattice is FINER by an integer factor. + * + *

The stratified lattice one level up reads correctly as randomness but produces no GROUPS, and + * groups are what a real sky has: a lattice caps density at about three times the mean, while an open + * cluster runs tens of times the field and a nucleus thousands. A cluster is the same seat one level + * down, and the whole mechanism is one word — commensurate.

+ * + *

Why commensurate, and why that makes it cheap

+ *

Each coarse super-cell inside a cluster is divided into {@code k³} sub-cells, so the fine lattice + * tiles exactly the coarse cells it replaces. There is no partial cell at the edge, no seam, and + * nothing to re-prove per ring — which is what a graded spacing would have cost. And because + * membership is decided per COARSE cell, a cell is wholly in a cluster or wholly out of it, so + * "which lattice does this coordinate live on" stays an O(1) question with one answer.

+ * + *

The sub-cell bounds are computed by proportioning rather than by dividing: sub-cell {@code i} + * runs from {@code floor(i·s/k)} to {@code floor((i+1)·s/k)}. That tiles a coarse cell of ANY edge + * exactly, including one that {@code k} does not divide — where a plain {@code s/k} would leave a + * remainder and a seam.

+ * + *

The separation floor becomes a property of a LATTICE LEVEL

+ *

Inside a globular's core stars really are closer together than a wide binary, and encounters + * really are frequent. The 10 000 AU floor is derived from whatever spacing is in force locally, so a + * clustered region gets a proportionally smaller one — and a system there loses outer bodies by the + * same rule that has always applied. A floor applied outside its domain of definition is precisely the + * mistake this design keeps removing elsewhere.

+ * + *

Immutable value type.

+ */ +public final class StarCluster { + + private final GalaxyGenConfig.ClusterType type; + private final long centreSuperX; + private final long centreSuperY; + private final long centreSuperZ; + private final long radiusSuperCells; + + public StarCluster(GalaxyGenConfig.ClusterType type, long centreSuperX, long centreSuperY, + long centreSuperZ, long radiusSuperCells) { + this.type = type; + this.centreSuperX = centreSuperX; + this.centreSuperY = centreSuperY; + this.centreSuperZ = centreSuperZ; + this.radiusSuperCells = Math.max(1L, radiusSuperCells); + } + + public GalaxyGenConfig.ClusterType type() { + return type; + } + + /** How many parts each coarse super-cell inside this cluster is divided into, per axis. */ + public int subdivision() { + return type.subdivision; + } + + /** Its radius, in COARSE super-cells — the unit its boundary is snapped to. */ + public long radiusSuperCells() { + return radiusSuperCells; + } + + public long centreSuperX() { + return centreSuperX; + } + + public long centreSuperY() { + return centreSuperY; + } + + public long centreSuperZ() { + return centreSuperZ; + } + + /** + * Whether this coarse super-cell is inside the cluster. + * + *

Rounded: the test is on the super-cell INDEX, so the boundary lands on coarse cell faces + * while the shape stays a ball rather than a box. That is what keeps the fine lattice exactly + * tiling and the answer per-cell.

+ */ + public boolean containsSuperCell(long supX, long supY, long supZ) { + double dx = supX - centreSuperX; + double dy = supY - centreSuperY; + double dz = supZ - centreSuperZ; + return dx * dx + dy * dy + dz * dz <= (double) radiusSuperCells * radiusSuperCells; + } + + /** + * The lower bound of sub-cell {@code index} inside a coarse cell of edge {@code coarseEdge}, + * as an offset from that cell's own low corner. + * + *

Proportioned, never divided: this tiles a coarse cell of any edge exactly, where + * {@code index · (coarseEdge / k)} would leave a remainder at the top of every cell.

+ */ + public long subCellLow(long index, long coarseEdge) { + return Math.floorDiv(index * coarseEdge, (long) subdivision()); + } + + /** The edge of sub-cell {@code index} — within one of the neighbouring sub-cells' edge. */ + public long subCellEdge(long index, long coarseEdge) { + return Math.max(1L, subCellLow(index + 1L, coarseEdge) - subCellLow(index, coarseEdge)); + } + + /** Which sub-cell an offset inside a coarse cell falls in, on one axis. */ + public long subCellIndex(long offsetInCoarse, long coarseEdge) { + long k = subdivision(); + long index = Math.floorDiv(offsetInCoarse * k, Math.max(1L, coarseEdge)); + return Math.min(k - 1L, Math.max(0L, index)); + } + + @Override + public String toString() { + return "StarCluster[" + type.name + " k=" + subdivision() + " r=" + radiusSuperCells + + " super-cells @ " + centreSuperX + "," + centreSuperY + "," + centreSuperZ + "]"; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java index ccc0231c2..6b8f1e4e6 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java @@ -102,22 +102,25 @@ public final class UniverseRegistry extends WorldSavedData implements CellFrames // ─── JVM-global seams / staging ─────────────────────────────────────────── private static volatile IGalaxyGenerator generator = new EmptyGalaxyGenerator(); - - /** - * The installed generator. The counterpart of {@link #setGenerator}: a caller that needs the - * partition the universe is laid out on — the super-cell edge, above all — has to be able to ask - * for it rather than assume a number that the configuration owns. - */ - public static IGalaxyGenerator generator() { - return generator; - } // How a stored star-id resolves to its content object. Defaults to the legacy catalogue; overridable so // the forward coord->system path is unit-testable without booting DimensionManager, and so an addon can // supply fabricated systems. private static volatile IntFunction starLookup = UniverseRegistry::lookupCatalogueStar; - private static Map pendingAnchors = new HashMap<>(); + private static Map pendingAnchors = new HashMap<>(); private static boolean pendingReset = false; + /** + * What each authored star was DECLARED as, keyed by star id — the galaxy-local form, kept so the + * catalogue can be written back in the language it was written in. Never persisted: it is re-read + * from XML on every load. + */ + private final Map declaredAnchors = new HashMap<>(); + + /** How this star was declared, if it was declared at all rather than given a fallback cell. */ + public GalacticAnchor declaredAnchorFor(int starId) { + return declaredAnchors.get(starId); + } + private static StellarBody lookupCatalogueStar(int starId) { return DimensionManager.getInstance().getStar(starId); } @@ -889,7 +892,14 @@ public boolean remove(GalacticCoord coord) { * are what makes a written-down coordinate keep denoting its body; clearing them here would mean * exactly the guarantee the store exists to give fails in the one case it is needed most.

*/ - public void applyAnchors(Map anchors, boolean reset) { + public void applyAnchors(Map anchors, boolean reset) { + // Remembered BEFORE the seeded early-return: the declaration is what the catalogue gets + // written back as, and on a restart the anchors are already placed while the XML still has to + // round-trip. It is re-read from XML on every load, which is exactly its lifetime. + declaredAnchors.clear(); + if (anchors != null) { + declaredAnchors.putAll(anchors); + } if (anchorsSeeded && !reset) { return; } @@ -897,16 +907,38 @@ public void applyAnchors(Map anchors, boolean reset) { List ids = new ArrayList<>(anchors.keySet()); Collections.sort(ids); for (Integer id : ids) { - GalacticCoord c = anchors.get(id); - if (c != null) { - place(c, id); + GalacticAnchor anchor = anchors.get(id); + if (anchor == null) { + continue; } + place(resolveAnchor(anchor, id), id); } } anchorsSeeded = true; markDirty(); } + /** + * Turn a galaxy-local declaration into the absolute cell name everything downstream uses. Done + * ONCE, here, at the reference angle — afterwards the authored system is named by a cell exactly + * like a procedural one and rotates with its galaxy exactly like one. + * + *

An anchor reaching past the radius its galaxy is GUARANTEED is a loud error and never a + * silent clamp: beyond that wall the position is valid on some seeds and intergalactic on others, + * and a pack author has to learn that from a log line rather than from a player's bug report.

+ */ + private GalacticCoord resolveAnchor(GalacticAnchor anchor, int starId) { + double guaranteed = generator.guaranteedAuthoredReachLy(); + if (guaranteed > 0d && anchor.reachLy() > guaranteed) { + LOGGER.error("star " + starId + " is authored at " + anchor + + ", which is " + (long) anchor.reachLy() + " light years from its galaxy's centre" + + " against a guaranteed radius of " + (long) guaranteed + ". On a seed whose" + + " galaxy comes out smaller than that, this system will sit in intergalactic" + + " space. Move it inside the guaranteed radius."); + } + return anchor.resolve(generator.declarationOriginOf(worldSeed, anchor.galaxy())); + } + /** * Give every catalogued star that still lacks a placement a deterministic fallback cell, so * planet→coord is total over the legacy galaxy. Sol (id 0) defaults to the origin; others take the @@ -955,8 +987,8 @@ public long worldSeed() { * Buffer XML-authored anchor coords parsed during {@code createAndLoadDimensions} (before worlds load, so * the registry is not yet reachable). Drained by {@link #populate} once worlds are up. */ - public static void stageAnchors(Map anchors, boolean reset) { - pendingAnchors = (anchors == null) ? new HashMap() : new HashMap<>(anchors); + public static void stageAnchors(Map anchors, boolean reset) { + pendingAnchors = (anchors == null) ? new HashMap() : new HashMap<>(anchors); pendingReset = reset; } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java index 72c4a1308..0a9073aa9 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java @@ -121,13 +121,47 @@ public final class UniverseScale { cellsForLightYears(MEAN_GALAXY_SEPARATION_LY); /** - * The radius the HOME galaxy is guaranteed to have at least, in light years. A galaxy's size is - * hash-drawn, so without a floor a pack that places authored content a few hundred light years - * out would work on one seed and put that content outside its own galaxy on the next. The floor - * is expressed as a constraint on which TYPES the home galaxy may be drawn from, never as a + * The radius a galaxy holding AUTHORED content is guaranteed to have at least, in light years. A + * galaxy's size is hash-drawn, so without a floor a pack that places content a few hundred light + * years out would work on one seed and put that content outside its own galaxy on the next. The + * floor is expressed as a constraint on which TYPES such a galaxy may be drawn from, never as a * clamp applied afterwards. */ - public static final double MIN_HOME_GALAXY_RADIUS_LY = 800d; + public static final double MIN_AUTHORED_GALAXY_RADIUS_LY = 900d; + + /** + * Where the universe ORIGIN sits inside the home galaxy, as a fraction of its radius — and it is + * emphatically not the centre. + * + *

The home galaxy is seated AROUND the origin rather than ON it, because the origin is where + * authored content lives and the centre of a galaxy is its nucleus: the densest, most violent + * place in it, and the last address a shipped solar system should have. Sol sits at about half + * the Milky Way's disc radius; this puts the origin in the same neighbourhood, out in the disc.

+ * + *

The offset lies IN the galaxy's plane, so the origin is disc material and not halo.

+ */ + public static final double HOME_GALAXY_ORIGIN_FRACTION = 0.55d; + + /** + * How far from the DECLARATION ORIGIN authored content is guaranteed to stay inside its galaxy, + * in light years. Derived, not chosen: it is what is left of the smallest galaxy that may hold + * authored content once the origin has been moved off its centre. + * + *

Beyond it a position is valid on some seeds and intergalactic on others, which is a thing an + * author must be TOLD rather than left to discover — hence a loud error and never a clamp.

+ */ + public static final double GUARANTEED_AUTHORED_REACH_LY = + (1d - HOME_GALAXY_ORIGIN_FRACTION) * MIN_AUTHORED_GALAXY_RADIUS_LY; + + /** + * The smallest lattice cell a system can be more than a lone star in, in cells. + * + *

Derived from the rest: a cell must leave room for a body at one orbit unit after the seat + * margin and the neighbourhood margin are taken out. It bounds how finely a star cluster may + * refine the lattice — a cluster cannot conjure room that its coarse cell never had, and a + * spacing too tight to be refined is a degenerate galaxy rather than an error.

+ */ + public static final long MIN_LATTICE_EDGE_CELLS = 9L; private UniverseScale() { } @@ -138,6 +172,12 @@ public static long cellsForLightYears(double lightYears) { return (long) Math.ceil(blocks / (double) GalacticCoord.CELL); } + /** A SIGNED length in light years as a whole number of cells, rounded to the nearest. */ + public static long cellsAt(double lightYears) { + return Math.round(lightYears * AstronomicalBodyHelper.BLOCKS_PER_LIGHT_YEAR + / (double) GalacticCoord.CELL); + } + /** The length in light years that {@code cells} cells span. */ public static double lightYearsForCells(double cells) { return cells * (double) GalacticCoord.CELL diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index 709bd82ad..17825f2ee 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -26,7 +26,9 @@ import zmaster587.advancedRocketry.dimension.TerrainSource; import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.GalacticAnchor; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.GalaxyKey; import zmaster587.advancedRocketry.universe.IGalaxyGenerator; import zmaster587.advancedRocketry.universe.PlanetTypePreset; import zmaster587.advancedRocketry.universe.PlanetTypes; @@ -64,6 +66,14 @@ public class XMLPlanetLoader { // between authored anchors; absent -> authored anchors only. All attrs are balance knobs with defaults. private static final String ELEMENT_GALAXYGEN = "galaxyGen"; private static final String ELEMENT_STARTYPE = "starType"; + private static final String ELEMENT_GALAXYTYPE = "galaxyType"; + private static final String ATTR_PROFILE = "profile"; + private static final String ATTR_MINRADIUS = "minRadius"; + private static final String ATTR_MAXRADIUS = "maxRadius"; + private static final String ATTR_THICKNESS = "thickness"; + private static final String ATTR_ARMS = "arms"; + private static final String ATTR_ROTATIONSPEED = "rotationSpeed"; + private static final String ATTR_COREFRACTION = "coreFraction"; // A planet TYPE preset: the named region of parameter space a world can land in, plus everything // that follows from being that kind of world. Present -> replaces the whole stock table. private static final String ELEMENT_PLANETTYPE = "planetType"; @@ -97,6 +107,7 @@ public class XMLPlanetLoader { // Explicit galactic address of an authored anchor system: "sectorX,sectorY,sectorZ" (cell indices). // Absent -> the system falls back to a deterministic cell (Sol -> origin). See UniverseRegistry. private static final String ATTR_GALACTIC_COORD = "galacticCoord"; + private static final String ATTR_GALAXY = "galaxy"; private static final String ATTR_SIZE = "size"; private static final String ATTR_NUMPLANETS = "numPlanets"; private static final String ATTR_NUMGASPLANETS = "numGasGiants"; @@ -183,7 +194,16 @@ public XMLPlanetLoader() { * Resolve a system's authored galactic coordinate from the live universe registry, or {@code null} when * no server/registry is reachable (so a no-server unit-test export simply omits the attribute). */ - private static GalacticCoord anchorCoordForWrite(int starId) { + /** + * How this star's address is written back. + * + *

In the language it was DECLARED in, when it was declared: a galaxy-local anchor writes its + * galaxy and its offset, and would otherwise be written as the absolute cell it resolved to and + * then read back on the next load as an offset from that same galaxy — shifted twice, and further + * every save. A star that was never declared writes the absolute cell it was given, which is what + * it has.

+ */ + private static GalacticAnchor anchorForWrite(int starId) { MinecraftServer server; try { server = FMLCommonHandler.instance().getMinecraftServerInstance(); @@ -199,7 +219,12 @@ private static GalacticCoord anchorCoordForWrite(int starId) { if (registry == null) { return null; } - return registry.coordForSystem(starId).orElse(null); + GalacticAnchor declared = registry.declaredAnchorFor(starId); + if (declared != null) { + return declared; + } + GalacticCoord placed = registry.coordForSystem(starId).orElse(null); + return placed == null ? null : GalacticAnchor.inHome(placed); } private static String attr(Node node, String name) { @@ -249,6 +274,25 @@ private static double attrDouble(Node node, String name, double def) { } } + /** + * The galaxy an authored anchor is declared against — {@code home} when unstated, which is what a + * pack that never thinks about galaxies gets and is always the right answer for it. + */ + private static GalaxyKey readGalaxyKey(Node node, String starName) { + String raw = attr(node, ATTR_GALAXY); + if (raw == null || raw.trim().isEmpty()) { + return GalaxyKey.HOME; + } + GalaxyKey key = GalaxyKey.parse(raw); + if (key == null) { + AdvancedRocketry.logger.warn("star '" + starName + "' names galaxy \"" + raw + + "\", which is neither \"" + GalaxyKey.HOME_NAME + "\" nor a \"gx,gy,gz\" lattice" + + " index. Placing it in the home galaxy."); + return GalaxyKey.HOME; + } + return key; + } + /** Parse a {@code } element (attrs + {@code } children) into a config. */ private GalaxyGenConfig readGalaxyGen(Node node) { GalaxyGenConfig defaults = GalaxyGenConfig.defaults(); @@ -269,10 +313,51 @@ private GalaxyGenConfig readGalaxyGen(Node node) { attrInt(child, ATTR_WEIGHT, 1))); } } - // An empty list falls back to the default archetypes (handled by the config ctor). - // The GALAXY archetype table is not authorable yet: it ships as a stock table in code, and a - // element is the natural place to override it when a pack needs to. - return new GalaxyGenConfig(minSpacing, density, galaxySpacing, galaxyDensity, types, null); + List galaxyTypes = new ArrayList<>(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (ELEMENT_GALAXYTYPE.equalsIgnoreCase(child.getNodeName())) { + galaxyTypes.add(readGalaxyType(child)); + } + } + // Empty / lists fall back to the stock archetypes (config ctor). + return new GalaxyGenConfig(minSpacing, density, galaxySpacing, galaxyDensity, types, + galaxyTypes); + } + + /** + * Parse one {@code } element into a galaxy archetype. + * + *
{@code
+     * 
+     * }
+ * + *

Every attribute defaults to the stock spiral's value, so a pack that wants to change only + * how flat a disc is writes only {@code thickness}.

+ */ + private static GalaxyGenConfig.GalaxyType readGalaxyType(Node node) { + String profileName = attr(node, ATTR_PROFILE); + GalaxyGenConfig.GalaxyProfile profile = GalaxyGenConfig.GalaxyProfile.DISC; + if (profileName != null && !profileName.trim().isEmpty()) { + try { + profile = GalaxyGenConfig.GalaxyProfile.valueOf(profileName.trim().toUpperCase()); + } catch (IllegalArgumentException bad) { + AdvancedRocketry.logger.warn("Unknown galaxy profile \"" + profileName + + "\" in ; using DISC"); + } + } + String name = attr(node, ATTR_NAME); + return new GalaxyGenConfig.GalaxyType( + (name == null || name.trim().isEmpty()) ? "Galaxy" : name.trim(), + profile, + attrDouble(node, ATTR_MINRADIUS, 900d), + attrDouble(node, ATTR_MAXRADIUS, 2200d), + attrDouble(node, ATTR_THICKNESS, 0.02d), + attrInt(node, ATTR_ARMS, 2), + attrDouble(node, ATTR_ROTATIONSPEED, 220d), + attrDouble(node, ATTR_COREFRACTION, 0.08d), + attrInt(node, ATTR_WEIGHT, 1)); } /** @@ -491,9 +576,60 @@ private static Element writeGalaxyGen(Document doc, GalaxyGenConfig cfg) { st.setAttribute(ATTR_WEIGHT, Integer.toString(t.weight)); e.appendChild(st); } + // The galaxy table is written back for the same reason the star table is: this file is + // REWRITTEN on every world save, so anything the reader did not turn into model state is lost. + // A pack that flattened its discs would silently get the stock ones back on the first save. + for (GalaxyGenConfig.GalaxyType t : cfg.galaxyTypes) { + Element gt = doc.createElement(ELEMENT_GALAXYTYPE); + gt.setAttribute(ATTR_NAME, t.name); + gt.setAttribute(ATTR_PROFILE, t.profile.name()); + gt.setAttribute(ATTR_MINRADIUS, Double.toString(t.minRadiusLy)); + gt.setAttribute(ATTR_MAXRADIUS, Double.toString(t.maxRadiusLy)); + gt.setAttribute(ATTR_THICKNESS, Double.toString(t.scaleHeightRatio)); + gt.setAttribute(ATTR_ARMS, Integer.toString(t.armCount)); + gt.setAttribute(ATTR_ROTATIONSPEED, Double.toString(t.rotationSpeedKmS)); + gt.setAttribute(ATTR_COREFRACTION, Double.toString(t.coreRadiusFraction)); + gt.setAttribute(ATTR_WEIGHT, Integer.toString(t.weight)); + e.appendChild(gt); + } return e; } + /** + * The two things a pack author has to know BEFORE the first save, written into the file itself. + * + *

Both are discoverable only from source otherwise, and by the time either is discovered the + * damage is done: the author has already placed a system in intergalactic space, or has already + * rerolled a universe that had a save attached to it. This file is rewritten on every world save, + * so the notice is emitted by the WRITER rather than shipped in a template that the first save + * would replace.

+ */ + private static final String AUTHORING_NOTICE = "\n" + + " READ BEFORE EDITING\n" + + "\n" + + " 1. A star's galacticCoord is GALAXY-LOCAL, not absolute. It is an offset in cells\n" + + " from the centre of the galaxy named by the star's `galaxy` attribute, which\n" + + " defaults to \"home\". The home galaxy is centred on the origin and always exists,\n" + + " whatever the world seed, and it is always at least 800 light years in radius, so\n" + + " anything you place inside that radius is valid on every seed. Beyond it your\n" + + " system may land in intergalactic space on some seeds; you get a loud error in the\n" + + " log if it does. Naming another galaxy (`galaxy=\"4,-1,2\"`) forces that lattice\n" + + " cell to hold one.\n" + + "\n" + + " Why: a galaxy fills about three thousandths of a percent of its own lattice cell,\n" + + " so a hand-picked absolute coordinate is in the void with probability 99.997%.\n" + + "\n" + + " 2. CHANGING ANY PARAMETER MID-SAVE IS UNDEFINED BEHAVIOUR. Nothing about\n" + + " a procedural system is stored: every star, planet and generated name is derived\n" + + " from (seed, coordinate) on every query. Change density, minSpacing, galaxySpacing,\n" + + " galaxyDensity or the archetype tables and you get a DIFFERENT UNIVERSE, in which\n" + + " every coordinate a player wrote down, every memory crystal and every route points\n" + + " at nothing. There is no migration and there cannot be one, because there is no old\n" + + " universe on disk to migrate. If you change these, start a new world.\n" + + "\n" + + " Comments you add to this file do not survive a world save; this one is regenerated.\n" + + " Full reference: docs/README_PLANETDEFS.md\n"; + public static String writeXML(IGalaxy galaxy) { Document doc; @@ -506,6 +642,7 @@ public static String writeXML(IGalaxy galaxy) { doc = docBuilder.newDocument(); Element galaxyElement = doc.createElement(ELEMENT_GALAXY); doc.appendChild(galaxyElement); + galaxyElement.appendChild(doc.createComment(AUTHORING_NOTICE)); Collection stars = galaxy.getStars(); @@ -517,9 +654,13 @@ public static String writeXML(IGalaxy galaxy) { nodeStar.setAttribute(ATTR_TEMP, Integer.toString(star.getTemperature())); nodeStar.setAttribute(ATTR_X, Integer.toString(star.getPosX())); nodeStar.setAttribute(ATTR_Y, Integer.toString(star.getPosZ())); - GalacticCoord starCoord = anchorCoordForWrite(star.getId()); - if (starCoord != null) { - nodeStar.setAttribute(ATTR_GALACTIC_COORD, UniverseRegistry.formatAnchor(starCoord)); + GalacticAnchor starAnchor = anchorForWrite(star.getId()); + if (starAnchor != null) { + nodeStar.setAttribute(ATTR_GALACTIC_COORD, + UniverseRegistry.formatAnchor(starAnchor.local())); + if (!starAnchor.galaxy().isHome()) { + nodeStar.setAttribute(ATTR_GALAXY, starAnchor.galaxy().toString()); + } } nodeStar.setAttribute(ATTR_SIZE, Float.toString(star.getSize())); nodeStar.setAttribute(ATTR_NUMPLANETS, "0"); @@ -1603,12 +1744,22 @@ public DimensionPropertyCoupling readAllPlanets() { StellarBody star = readStar(masterNode); coupling.stars.add(star); - // Explicit galactic address for this authored anchor (optional). Staged into the universe - // registry after the catalogue is built; absent -> a deterministic fallback cell downstream. + // Explicit galactic address for this authored anchor (optional). It is GALAXY-LOCAL: an + // offset from the centre of the galaxy named by `galaxy` (default `home`), not an absolute + // cell. A galaxy fills about three thousandths of a percent of its own lattice cell, so an + // absolute declaration would land in intergalactic space on virtually every seed. + // + // Resolved into an absolute cell once, at population, when the world seed is known — the + // galaxy's centre is a hash draw and cannot be known here. if (masterNode.hasAttributes()) { Node coordNode = masterNode.getAttributes().getNamedItem(ATTR_GALACTIC_COORD); if (coordNode != null && !coordNode.getNodeValue().isEmpty()) { - coupling.anchorCoords.put(star.getId(), UniverseRegistry.parseAnchor(coordNode.getNodeValue())); + GalaxyKey key = readGalaxyKey(masterNode, star.getName()); + coupling.anchorCoords.put(star.getId(), GalacticAnchor.of(key, + UniverseRegistry.parseAnchor(coordNode.getNodeValue()))); + if (!key.isHome() && !coupling.declaredGalaxies.contains(key)) { + coupling.declaredGalaxies.add(key); + } } } @@ -1637,6 +1788,13 @@ public DimensionPropertyCoupling readAllPlanets() { masterNode = masterNode.getNextSibling(); } + // Every galaxy an anchor named is RESERVED. The keys are only known once the catalogue has + // been walked, which is after was read — so they are folded in here rather than + // making the document's element ORDER load-bearing. + if (coupling.galaxyGenConfig != null && !coupling.declaredGalaxies.isEmpty()) { + coupling.galaxyGenConfig = + coupling.galaxyGenConfig.withReservedGalaxies(coupling.declaredGalaxies); + } return coupling; } @@ -1668,7 +1826,11 @@ public static class DimensionPropertyCoupling { public List dims = new LinkedList<>(); // Authored galactic addresses, keyed by star id (parse order). Only anchors that declared an // explicit appear here; the rest get a deterministic fallback at population. - public Map anchorCoords = new HashMap<>(); + // GALAXY-LOCAL: resolved into absolute cells at population, once the world seed is known. + public Map anchorCoords = new HashMap<>(); + // Every non-home galaxy an anchor named. Each one is RESERVED — its cell holds a galaxy + // whatever the hash says — because authored content must exist under every seed. + public List declaredGalaxies = new ArrayList<>(); // Procedural-galaxy generation config from an optional element; null = authored-only. public GalaxyGenConfig galaxyGenConfig = null; // Authored presets. Empty -> the stock table stands. diff --git a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java index 302d3ac35..be953ba46 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java @@ -154,8 +154,14 @@ public void oneOrbitalDistanceMeansOneDistanceInBothFamilies() { GalaxyGenConfig.DEFAULT_GALAXY_SPACING, GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, null, null)); long spacing = GalaxyGenConfig.DEFAULT_MIN_SPACING; - Optional seat = gen.anchorAt(0xBEEFL, - GalacticCoord.ofSectorLocal(spacing, spacing, spacing, 0L, 0L, 0L)); + // SWEEP for an occupied super-cell rather than demanding one particular cube. Occupancy is a + // draw scaled by the galaxy's profile, so any single cube is a coin toss and a fixture that + // insists on one is testing the coin. + Optional seat = Optional.empty(); + for (long i = 1; i <= 8 && !seat.isPresent(); i++) { + seat = gen.anchorAt(0xBEEFL, + GalacticCoord.ofSectorLocal(i * spacing, spacing, spacing, 0L, 0L, 0L)); + } assertTrue("the fixture needs an occupied super-cell", seat.isPresent()); int compared = 0; for (SystemBody b : gen.bodiesFor(0xBEEFL, seat.get())) { diff --git a/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java b/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java index bfa15d152..230f8cd7b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java @@ -16,7 +16,9 @@ import zmaster587.advancedRocketry.dimension.TerrainSource; import zmaster587.advancedRocketry.test.MinecraftBootstrap; import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.GalacticAnchor; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.GalaxyKey; import zmaster587.advancedRocketry.universe.IGalaxyGenerator; import zmaster587.advancedRocketry.universe.UniverseRegistry; import zmaster587.advancedRocketry.util.XMLPlanetLoader; @@ -531,6 +533,108 @@ public void galaxyGenElementParsesIntoConfig() throws IOException { assertEquals(3, c.galaxyGenConfig.starTypes.get(0).weight); } + @Test + public void galaxyTypeChildrenReplaceTheStockTable() throws Exception { + // The one table a pack is most likely to want to touch, and the reason it is authorable at + // all: how flat a disc is has no derivation — it is a free parameter of the shape. + DimensionPropertyCoupling c = parse(galaxy( + "\n" + + " \n" + + "\n")); + assertNotNull(c.galaxyGenConfig); + assertEquals("one must REPLACE the stock table, not extend it", 1, + c.galaxyGenConfig.galaxyTypes.size()); + GalaxyGenConfig.GalaxyType t = c.galaxyGenConfig.galaxyTypes.get(0); + assertEquals("Fat Disc", t.name); + assertEquals(GalaxyGenConfig.GalaxyProfile.DISC, t.profile); + assertEquals(1000d, t.minRadiusLy, 1e-9); + assertEquals(1800d, t.maxRadiusLy, 1e-9); + assertEquals("disc thickness must be what the pack asked for", 0.25d, t.scaleHeightRatio, 1e-9); + assertEquals(3, t.armCount); + assertEquals(180d, t.rotationSpeedKmS, 1e-9); + assertEquals(0.2d, t.coreRadiusFraction, 1e-9); + assertEquals(5, t.weight); + } + + @Test + public void galaxyTypesRoundTripThroughWriteXml() throws IOException { + // This file is REWRITTEN on every world save, so a table the writer does not emit is a table a + // pack silently loses the first time anybody saves. + GalaxyGenConfig parsed = parse(galaxy( + "\n" + + " \n" + + " \n" + + "\n")).galaxyGenConfig; + try { + UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(parsed)); + String written = XMLPlanetLoader.writeXML(DimensionManager.getInstance()); + File f = tempFolder.newFile(); + Files.write(f.toPath(), written.getBytes(StandardCharsets.UTF_8)); + XMLPlanetLoader loader = new XMLPlanetLoader(); + assertTrue(loader.loadFile(f)); + GalaxyGenConfig round = loader.readAllPlanets().galaxyGenConfig; + + assertNotNull(round); + assertEquals(2, round.galaxyTypes.size()); + assertEquals("Thin", round.galaxyTypes.get(0).name); + assertEquals(0.005d, round.galaxyTypes.get(0).scaleHeightRatio, 1e-9); + assertEquals(GalaxyGenConfig.GalaxyProfile.SPHEROID, round.galaxyTypes.get(1).profile); + assertEquals(11, round.galaxyTypes.get(1).weight); + } finally { + UniverseRegistry.setGenerator(null); + } + } + + @Test + public void anAuthoredAnchorIsDeclaredAgainstAGalaxy() throws Exception { + // A galaxy fills about three thousandths of a percent of its own lattice cell, so an absolute + // declaration would land in intergalactic space on virtually every seed. An unqualified + // declaration means `home`, which is what a pack that never thinks about galaxies gets. + DimensionPropertyCoupling c = parse(galaxy( + "\n" + + "\n" + + "\n")); + assertEquals(2, c.anchorCoords.size()); + GalacticAnchor sol = c.anchorCoords.get(c.stars.get(0).getId()); + GalacticAnchor far = c.anchorCoords.get(c.stars.get(1).getId()); + assertTrue("an unqualified anchor lives in the home galaxy", sol.galaxy().isHome()); + assertEquals(GalaxyKey.of(4L, -1L, 2L), far.galaxy()); + assertEquals(500L, far.local().sectorX()); + + assertEquals("every non-home galaxy an anchor named must be reserved", 1, + c.declaredGalaxies.size()); + assertTrue("and the config must carry it, so its cell is seated on every seed", + c.galaxyGenConfig.reservedGalaxies.contains(GalaxyKey.of(4L, -1L, 2L))); + } + + @Test + public void theWrittenCatalogueTellsAnAuthorWhatItCostsToEditIt() throws Exception { + // Both facts are otherwise discoverable only from source, and by then the damage is done: the + // system is already in the void, or the universe is already rerolled under a live save. The + // WRITER emits it, because this file is rewritten on every save and a shipped template would + // be replaced by the first one. + String written = XMLPlanetLoader.writeXML(DimensionManager.getInstance()); + assertTrue("the written catalogue must say an anchor is galaxy-local: " + written, + written.contains("GALAXY-LOCAL")); + assertTrue("and that the home galaxy always exists", + written.contains("home") && written.contains("800 light years")); + assertTrue("and that changing a generator parameter mid-save is undefined", + written.contains("UNDEFINED BEHAVIOUR")); + + // And it must still be a document that parses, or the notice would cost the catalogue. + File f = tempFolder.newFile(); + Files.write(f.toPath(), written.getBytes(StandardCharsets.UTF_8)); + XMLPlanetLoader loader = new XMLPlanetLoader(); + assertTrue("a catalogue carrying the notice must still load", loader.loadFile(f)); + assertNotNull(loader.readAllPlanets()); + } + @Test public void absentGalaxyGenLeavesConfigNull() throws IOException { DimensionPropertyCoupling c = parse(galaxy(star("Sol", ""))); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java index 8300cb4e1..a170a6eb0 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java @@ -2,15 +2,21 @@ import org.junit.Test; +import java.util.Collections; import java.util.HashSet; import java.util.Optional; import java.util.Set; import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Cosmology; +import zmaster587.advancedRocketry.universe.GalacticAnchor; +import zmaster587.advancedRocketry.universe.GalacticFrame; +import zmaster587.advancedRocketry.universe.GalaxyKey; import zmaster587.advancedRocketry.universe.Galaxy; import zmaster587.advancedRocketry.universe.GalaxyField; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.LightYearVector; import zmaster587.advancedRocketry.universe.UniverseScale; import static org.junit.Assert.assertEquals; @@ -47,11 +53,20 @@ public void theHomeGalaxyExistsUnderEverySeed() { for (long seed = 1L; seed <= 200L; seed++) { Galaxy home = f.home(seed); assertNotNull("seed " + seed + " has no home galaxy", home); - assertEquals("the home galaxy is centred on the origin", GalacticCoord.ORIGIN.cellKey(), - home.centre().cellKey()); assertTrue("seed " + seed + "'s home galaxy is only " + home.radiusLy() + " ly across, under the guaranteed minimum", - home.radiusLy() >= UniverseScale.MIN_HOME_GALAXY_RADIUS_LY); + home.radiusLy() >= UniverseScale.MIN_AUTHORED_GALAXY_RADIUS_LY); + assertTrue("the ORIGIN must be inside the home galaxy under seed " + seed, + home.containsSector(0L, 0L, 0L)); + // And out in the disc, not at the centre: the centre of a galaxy is its nucleus, which is + // the last address a shipped solar system should have. + double originRadius = home.localRadius( + -UniverseScale.lightYearsForCells(home.centre().sectorX()), + -UniverseScale.lightYearsForCells(home.centre().sectorY()), + -UniverseScale.lightYearsForCells(home.centre().sectorZ())); + assertEquals("the origin must sit at a sun-like galactic radius", + UniverseScale.HOME_GALAXY_ORIGIN_FRACTION * home.radiusLy(), originRadius, + home.radiusLy() * 1e-3d); } } @@ -314,6 +329,199 @@ public void aGalaxyHoldsAPopulationOfTheRightOrder() { + "lattice was sized for", systems < 1e7d); } + // ─── The intergalactic regime (R3 + R8) ──────────────────────────────────── + + @Test + public void theHomeGalaxyHasNoMotionOfItsOwn() { + // It is the rest frame everything else is measured against: every other galaxy moves relative + // to it, which is also what an observer actually sees. What must NOT happen is authored + // content being left behind by its own galaxy — so the check is that the origin keeps its + // place INSIDE the galaxy, not that the galaxy sits still on a static grid it does not live on. + GalaxyField f = field(GalaxyGenConfig.DEFAULT_GALAXY_DENSITY); + for (long seed = 1L; seed <= 30L; seed++) { + Galaxy home = f.home(seed); + assertEquals("the home galaxy must have no peculiar velocity", 0d, + home.peculiarVelocity().length(), 0d); + double at0 = home.boundPositionOfCellAt(GalacticCoord.ORIGIN, 0L) + .distanceTo(home.centreAt(0L)); + for (long t : new long[] {1_000_000L, 1_000_000_000_000L}) { + assertEquals("authored content must ride its galaxy, not be left behind by it", at0, + home.boundPositionOfCellAt(GalacticCoord.ORIGIN, t).distanceTo(home.centreAt(t)), + at0 * 1e-9d); + } + } + } + + @Test + public void aGalaxyCannotDriftOutOfItsOwnCell() { + // The invariant peculiar velocity threatens: a galaxy that wandered into a neighbouring cell + // would break at-most-one-per-cell, non-overlap, AND the O(1) ownership answer at once. The + // bound is real code, and it is measured here rather than asserted — at realistic speeds it is + // orders away from binding, which is the finding. + GalaxyGenConfig config = cfg(1.0d); + GalaxyField f = new GalaxyField(config); + double halfCellLy = UniverseScale.lightYearsForCells(config.galaxySpacing / 2d); + double worstFraction = 0d; + int checked = 0; + for (long gx = -4L; gx <= 4L; gx++) { + for (long gy = -2L; gy <= 2L; gy++) { + Optional g = f.galaxyAtIndex(2024L, gx, gy, 0L); + if (!g.isPresent() || GalaxyField.isHomeCell(gx, gy, 0L)) { + continue; + } + double drift = g.get().peculiarVelocity().length() + * (double) Cosmology.DRIFT_HORIZON_TICKS; + double room = halfCellLy - g.get().radiusLy(); + assertTrue(g.get() + " drifts " + drift + " ly against " + room + " ly of room", + drift <= room); + worstFraction = Math.max(worstFraction, drift / room); + checked++; + } + } + assertTrue(checked > 5); + System.out.println("worst galaxy drift over the horizon: " + + String.format("%.3e", worstFraction) + " of its available room"); + } + + @Test + public void aGalaxyDrawsARealisticPeculiarVelocity() { + GalaxyField f = field(1.0d); + int checked = 0; + for (long gx = -5L; gx <= 5L; gx++) { + Optional g = f.galaxyAtIndex(555L, gx, 3L, 0L); + if (!g.isPresent() || GalaxyField.isHomeCell(gx, 3L, 0L)) { + continue; + } + // 50..600 km/s, expressed in this layer's unit. + double speed = g.get().peculiarVelocity().length(); + assertTrue("a galaxy must actually move", speed > 0d); + assertTrue("and not faster than the band allows", + speed <= UniverseScale.lightYearsPerTick(600d) * 1.000001d); + checked++; + } + assertTrue(checked > 3); + } + + @Test + public void aPointIsEitherBoundToItsGalaxyOrComovingInTheVoid() { + // Two states and no third: there is no "nowhere". Every point belongs to exactly one galaxy + // CELL, and inside that cell it is either in the galaxy or in the void of it. + GalaxyField f = field(1.0d); + Galaxy home = f.home(11L); + GalacticCoord inside = GalacticCoord.ofSectorLocal( + UniverseScale.cellsForLightYears(home.radiusLy() * 0.5d), 0L, 0L, 0L, 0L, 0L); + GalacticCoord outside = GalacticCoord.ofSectorLocal( + UniverseScale.cellsForLightYears(home.radiusLy() * 4d), 0L, 0L, 0L, 0L, 0L); + + assertEquals(GalacticFrame.GALACTIC, f.frameAt(11L, inside)); + assertEquals(GalacticFrame.COMOVING, f.frameAt(11L, outside)); + } + + @Test + public void aBoundPointRotatesAndAVoidPointDoesNot() { + // The two laws, told apart by what they DO. A bound point turns with the disc and keeps its + // distance from the centre; a void point is carried by the Hubble flow and never rotates. + GalaxyField f = field(1.0d); + Galaxy home = f.home(11L); + long boundCells = UniverseScale.cellsForLightYears(home.radiusLy() * 0.5d); + GalacticCoord bound = GalacticCoord.ofSectorLocal(boundCells, 0L, 0L, 0L, 0L, 0L); + long t = 200_000_000_000L; // long enough that the slow rotation is measurable + + LightYearVector at0 = f.positionAt(11L, bound, 0L); + LightYearVector later = f.positionAt(11L, bound, t); + assertTrue("a bound point must move with the disc", later.distanceTo(at0) > 0d); + assertEquals("and keep its radius from the centre, because a galaxy does not expand", + at0.distanceTo(home.centreAt(0L)), later.distanceTo(home.centreAt(t)), + home.radiusLy() * 1e-9d); + + GalacticCoord voidCell = GalacticCoord.ofSectorLocal( + UniverseScale.cellsForLightYears(home.radiusLy() * 4d), 0L, 0L, 0L, 0L, 0L); + LightYearVector voidAt0 = f.positionAt(11L, voidCell, 0L); + LightYearVector voidLater = f.positionAt(11L, voidCell, t); + assertEquals("a void point is carried straight outwards, never sideways", 0d, + voidLater.y(), 1e-9d); + assertEquals("a void point is carried straight outwards, never sideways", 0d, + voidLater.z(), 1e-9d); + assertTrue("and it is carried by the Hubble flow", voidLater.x() > voidAt0.x()); + assertEquals("by exactly the scale factor", voidAt0.x() * Cosmology.scaleFactorAt(t), + voidLater.x(), voidAt0.x() * 1e-12d); + } + + // ─── Authored content is declared against a galaxy (R11) ─────────────────── + + @Test + public void aDeclaredGalaxyIsSeatedWhateverTheHashSays() { + // A galaxy is a hash draw and may simply not be there under another seed, while authored + // content must exist under EVERY seed. So naming a galaxy in the catalogue reserves its cell. + long seed = 424242L; + GalaxyField plain = field(0.2d); + GalaxyKey empty = null; + for (long gx = 1L; gx <= 40L && empty == null; gx++) { + if (!plain.galaxyAtIndex(seed, gx, 0L, 0L).isPresent()) { + empty = GalaxyKey.of(gx, 0L, 0L); + } + } + assertNotNull("the sweep must find a void galaxy cell to reserve", empty); + + GalaxyGenConfig reserved = cfg(0.2d).withReservedGalaxies(Collections.singletonList(empty)); + GalaxyField withKey = new GalaxyField(reserved); + assertTrue("a declared key must force its cell to hold a galaxy", + withKey.galaxyAtIndex(seed, empty.gx(), empty.gy(), empty.gz()).isPresent()); + assertTrue(withKey.isReserved(empty.gx(), empty.gy(), empty.gz())); + assertTrue("and it must be reachable by key", withKey.declarationOriginOf(seed, empty).isPresent()); + } + + @Test + public void aGalaxyHoldingAuthoredContentIsDrawnBigEnoughForIt() { + // The guarantee is a constraint on the type DRAW, never a clamp applied afterwards: a pack + // that places a system 700 light years out must work on every seed. + long seed = 909L; + GalaxyKey key = GalaxyKey.of(6L, -2L, 3L); + GalaxyField f = new GalaxyField( + cfg(0.2d).withReservedGalaxies(Collections.singletonList(key))); + Galaxy declared = f.galaxyAtIndex(seed, key.gx(), key.gy(), key.gz()).get(); + assertTrue("a reserved galaxy is only " + declared.radiusLy() + " ly across", + declared.radiusLy() >= UniverseScale.MIN_AUTHORED_GALAXY_RADIUS_LY); + } + + @Test + public void aHomeDeclarationResolvesToItself() { + // This is what centring the home galaxy on the ORIGIN buys, and it is the whole migration + // story: a coordinate authored before galaxies existed means exactly what it used to. + GalaxyField f = field(GalaxyGenConfig.DEFAULT_GALAXY_DENSITY); + GalacticCoord local = GalacticCoord.ofSectorLocal(1_500_000L, -20_000L, 7L, 0L, 0L, 0L); + GalacticAnchor anchor = GalacticAnchor.inHome(local); + assertEquals(local.cellKey(), + anchor.resolve(f.declarationOriginOf(3L, GalaxyKey.HOME)).cellKey()); + } + + @Test + public void aDeclarationInAnotherGalaxyResolvesAgainstThatGalaxysCentre() { + long seed = 77L; + GalaxyKey key = GalaxyKey.of(2L, 0L, 0L); + GalaxyField f = new GalaxyField( + cfg(1.0d).withReservedGalaxies(Collections.singletonList(key))); + GalacticCoord centre = f.centreOf(seed, key).get(); + GalacticCoord local = GalacticCoord.ofSectorLocal(500_000L, 0L, 0L, 0L, 0L, 0L); + + GalacticCoord resolved = + GalacticAnchor.of(key, local).resolve(f.declarationOriginOf(seed, key)); + assertEquals(centre.sectorX() + 500_000L, resolved.sectorX()); + assertTrue("and it must land inside the galaxy it named", + f.galaxyAtIndex(seed, key.gx(), key.gy(), key.gz()).get() + .containsSector(resolved.sectorX(), resolved.sectorY(), resolved.sectorZ())); + } + + @Test + public void withNoGalaxyTierADeclarationIsAlreadyAbsolute() { + // An authored-only universe has nothing for a declaration to be local TO, so local and + // absolute coincide — which is both the only reading that can be right and the behaviour that + // existed before galaxies did. + GalacticCoord local = GalacticCoord.ofSectorLocal(42L, -7L, 3L, 0L, 0L, 0L); + assertEquals(local.cellKey(), + GalacticAnchor.inHome(local).resolve(Optional.empty()).cellKey()); + } + private static int countGalaxies(GalaxyField f, long seed) { int found = 0; for (long gx = -5L; gx <= 5L; gx++) { diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java index 483bd2606..7b0da6255 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java @@ -3,8 +3,10 @@ import org.junit.Test; import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.Cosmology; import zmaster587.advancedRocketry.universe.Galaxy; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.LightYearVector; import zmaster587.advancedRocketry.universe.UniverseScale; import static org.junit.Assert.assertEquals; @@ -43,7 +45,13 @@ private static GalaxyGenConfig.GalaxyType dwarf() { /** A galaxy with its plane on the world's XZ plane, so a test can reason in plain coordinates. */ private static Galaxy flat(GalaxyGenConfig.GalaxyType type) { return new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, type, RADIUS, 0d, 0d, - Math.toRadians(20d), 0d); + Math.toRadians(20d), 0d, LightYearVector.ZERO); + } + + /** The same galaxy, seated away from the origin and moving — the subject of the R3 laws. */ + private static Galaxy adrift(GalacticCoord seat, LightYearVector velocity) { + return new Galaxy(1L, 0L, 0L, seat, smoothDisc(), RADIUS, 0d, 0d, Math.toRadians(20d), 0d, + velocity); } @Test @@ -123,9 +131,9 @@ public void orientationRotatesTheDiscWithoutChangingItsShape() { // Two galaxies alike but for their orientation must be the same object seen from elsewhere: // the density a point sees depends on where it is IN THE GALAXY, never on the world axes. Galaxy flat = new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, 0d, 0d, - Math.toRadians(20d), 0d); + Math.toRadians(20d), 0d, LightYearVector.ZERO); Galaxy tilted = new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, - Math.toRadians(90d), 0d, Math.toRadians(20d), 0d); + Math.toRadians(90d), 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO); // The tilted galaxy's pole is +X, so ITS plane is the world's YZ plane. double r = RADIUS * 0.3d; assertEquals("the same point of the galaxy must read the same however it is oriented", @@ -187,6 +195,74 @@ public void rotationIsSlowEnoughToBeInvisibleWithinASave() { assertFalse("but it must be a finite number of ticks", Double.isInfinite(turnTicks)); } + // ─── Expansion and peculiar motion (R3) ──────────────────────────────────── + + @Test + public void expansionIsMonotoneAndStartsAtOne() { + // t = 0 is world creation, so the universe's age IS the save's age. And a(t) only ever grows: + // shear separates reversibly (theta wraps), expansion does not. A galaxy that recedes past a + // drive's reach has receded permanently, which is a stronger claim than "the sky moves". + assertEquals("a(0) must be exactly 1", 1d, Cosmology.scaleFactorAt(0L), 0d); + double previous = 1d; + for (long t = 1_000_000L; t <= 1_000_000_000_000L; t *= 10L) { + double a = Cosmology.scaleFactorAt(t); + assertTrue("a(" + t + ") = " + a + " did not grow past " + previous, a > previous); + previous = a; + } + } + + @Test + public void expansionCarriesTheCentreAndNothingInsideTheGalaxy() { + // The whole reason expansion is applied to the CENTRE only: a bound system does not expand, + // and scaling intra-galactic coordinates would grow every r and corrupt omega(r) from within. + // Measured as the separation between two bound points, which must not change with the scale + // factor even while their galaxy is being carried away. + Galaxy g = adrift(GalacticCoord.ofSectorLocal(4_000_000_000L, 0L, 0L, 0L, 0L, 0L), + LightYearVector.of(1e-9d, 0d, 0d)); + double r = RADIUS * 0.4d; + long far = 500_000_000L; + + // Two points at the same radius, so rotation carries them equally and only expansion could + // separate them. + double now = g.boundPositionAt(0L, r, 0d, 0d).distanceTo(g.boundPositionAt(0L, r, 1d, 0d)); + double later = g.boundPositionAt(far, r, 0d, 0d).distanceTo(g.boundPositionAt(far, r, 1d, 0d)); + assertEquals("two bound points must keep their separation while their galaxy is carried away", + now, later, now * 1e-9d); + assertTrue("and the galaxy itself must have moved", + g.centreAt(far).distanceTo(g.centreAt(0L)) > 0d); + } + + @Test + public void aGalaxyMovesUnderBothExpansionAndItsOwnVelocity() { + // Expansion alone lets a galaxy only RECEDE, so an approaching neighbour would be + // unrepresentable — and at short range peculiar motion dominates expansion in a real group. + GalacticCoord seat = GalacticCoord.ofSectorLocal(4_000_000_000L, 0L, 0L, 0L, 0L, 0L); + Galaxy still = adrift(seat, LightYearVector.ZERO); + Galaxy inbound = adrift(seat, LightYearVector.of(-1e-9d, 0d, 0d)); + long t = 100_000_000L; + + double seatLy = still.centreAt(0L).x(); + assertTrue("expansion alone can only push a galaxy outwards", + still.centreAt(t).x() > seatLy); + assertTrue("but its own velocity must be able to bring it closer", + inbound.centreAt(t).x() < seatLy); + } + + @Test + public void theCentreLawIsEvaluatedNeverIntegrated() { + // Analytic in t, like everything else in this layer: asking for tick N is one evaluation, so + // there is no step size and nothing to accumulate. + Galaxy g = adrift(GalacticCoord.ofSectorLocal(2_000_000_000L, 0L, 0L, 0L, 0L, 0L), + LightYearVector.of(3e-10d, -1e-10d, 2e-10d)); + long t = 12_345_678L; + double a = Cosmology.scaleFactorAt(t); + LightYearVector expected = LightYearVector.ofCell(g.centre()) + .plus(g.peculiarVelocity().scale((double) t)).scale(a); + assertEquals(expected.x(), g.centreAt(t).x(), Math.abs(expected.x()) * 1e-12d); + assertEquals(expected.y(), g.centreAt(t).y(), 1e-9d); + assertEquals(expected.z(), g.centreAt(t).z(), 1e-9d); + } + @Test public void aSectorReadingAgreesWithTheLengthItStandsFor() { // The generator asks in cell names; everything above is written in light years. The two have diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java new file mode 100644 index 000000000..d5b5707f5 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java @@ -0,0 +1,252 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import java.util.Optional; + +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.ClusterField; +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Galaxy; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.StarCluster; +import zmaster587.advancedRocketry.universe.UniverseScale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for star clusters — the seat one level BELOW the star lattice. + * + *

What is pinned is the mechanism, not the richness: the fine lattice TILES the coarse cells it + * replaces exactly (no seam, no partial cell, no overlap), membership is a property of the COARSE cell + * so ownership stays one question with one answer, the separation floor follows the LOCAL lattice + * level rather than a global constant, and a cluster cannot refine a cell below what a system needs. + * The subdivisions and radii are balance knobs and are fed in as inputs.

+ */ +public class StarClusterTest { + + private static final long SEED = 0x51A25L; + + private static GalaxyGenConfig cfg() { + return GalaxyGenConfig.defaults(); + } + + private static GalaxyGenConfig.ClusterType type(int k) { + return new GalaxyGenConfig.ClusterType("Test", k, 5d, 15d, 1); + } + + // ─── The commensurate construction ───────────────────────────────────────── + + @Test + public void theFineLatticeTilesACoarseCellExactly() { + // The one word the whole mechanism rests on. If the sub-cells did not tile, every coarse cell + // would carry a partial cell at its top face and the boundary would need a rule of its own — + // which is exactly the cost a graded spacing was rejected for. + for (int k : new int[] {2, 3, 4, 7, 14, 25, 215}) { + for (long coarseEdge : new long[] {1_000L, 40_018_890L, 999_983L}) { + StarCluster c = new StarCluster(type(k), 0L, 0L, 0L, 3L); + long covered = 0L; + long previousHigh = 0L; + for (long i = 0; i < k; i++) { + long low = c.subCellLow(i, coarseEdge); + long edge = c.subCellEdge(i, coarseEdge); + assertEquals("sub-cell " + i + " must start where " + (i - 1) + " ended", + previousHigh, low); + previousHigh = low + edge; + covered += edge; + } + assertEquals("k=" + k + " over an edge of " + coarseEdge + " must tile it exactly", + coarseEdge, covered); + } + } + } + + @Test + public void everyOffsetLandsInExactlyOneSubCell() { + // The inverse of the tiling: a coordinate must resolve to one sub-cell, and that sub-cell must + // be the one whose bounds contain it. A mismatch here is a system addressed by a cell it does + // not sit in. + long coarseEdge = 40_018_890L; + StarCluster c = new StarCluster(type(25), 0L, 0L, 0L, 3L); + for (long offset : new long[] {0L, 1L, coarseEdge / 3L, coarseEdge / 2L, coarseEdge - 1L}) { + long i = c.subCellIndex(offset, coarseEdge); + assertTrue("index " + i + " out of range for offset " + offset, i >= 0 && i < 25); + assertTrue("offset " + offset + " is not inside the sub-cell it resolved to", + offset >= c.subCellLow(i, coarseEdge) + && offset < c.subCellLow(i, coarseEdge) + c.subCellEdge(i, coarseEdge)); + } + } + + @Test + public void membershipIsAPropertyOfTheCoarseCell() { + // Snapped to coarse cell faces, which is what makes the fine lattice tile and what keeps + // "which lattice does this coordinate live on" an O(1) question with one answer. The shape + // stays a ball, because the test is on the super-cell INDEX rather than on a box. + StarCluster c = new StarCluster(type(4), 10L, 10L, 10L, 3L); + assertTrue(c.containsSuperCell(10L, 10L, 10L)); + assertTrue(c.containsSuperCell(13L, 10L, 10L)); + assertFalse(c.containsSuperCell(14L, 10L, 10L)); + assertFalse("a ball, not a box: the corner is outside", c.containsSuperCell(13L, 13L, 13L)); + } + + // ─── Seating ─────────────────────────────────────────────────────────────── + + @Test + public void everyGalaxyHasANucleusAtItsOwnCentre() { + // Not a special case: the nucleus is a cluster like the others, drawn at a known place instead + // of a drawn one, and it is the richest of them. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg()); + ClusterField clusters = gen.clusters(); + Galaxy home = gen.galaxies().home(SEED); + Optional nucleus = clusters.nucleusOf(SEED, home); + assertTrue(nucleus.isPresent()); + assertEquals(GalaxyGenConfig.NUCLEUS.subdivision, nucleus.get().subdivision()); + + long s = cfg().minSpacing; + assertTrue("the nucleus must cover the galaxy's own centre", + nucleus.get().containsSuperCell(Math.floorDiv(home.centre().sectorX(), s), + Math.floorDiv(home.centre().sectorY(), s), + Math.floorDiv(home.centre().sectorZ(), s))); + assertTrue("and it must be the richest cluster there is", + nucleus.get().subdivision() > cfg().clusterTypes.get(0).subdivision); + } + + @Test + public void aClusterNeverStraddlesItsOwnLatticeCell() { + // The same containment the galaxy tier needs, one level down and for the same reason: a + // cluster reaching into a neighbouring cluster cell would make ownership ambiguous. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg()); + ClusterField clusters = gen.clusters(); + Galaxy home = gen.galaxies().home(SEED); + long spacing = clusters.spacingSuperCells(); + int checked = 0; + for (long cx = -3L; cx <= 3L; cx++) { + for (long cy = -2L; cy <= 2L; cy++) { + Optional c = clusters.clusterAtIndex(SEED, home, cx, cy, 0L); + if (!c.isPresent()) { + continue; + } + long r = c.get().radiusSuperCells(); + assertTrue("a cluster reaches past its cell's low face", + c.get().centreSuperX() - r >= cx * spacing); + assertTrue("a cluster reaches past its cell's high face", + c.get().centreSuperX() + r <= cx * spacing + spacing - 1L); + checked++; + } + } + assertTrue("the sweep must find clusters", checked > 3); + } + + @Test + public void clustersOnlyExistWhereTheirGalaxyHasStars() { + // One function, not a second rule: a cluster's occupancy is scaled by the same density profile + // that placed the systems, so clusters stop where the galaxy does. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg()); + Galaxy home = gen.galaxies().home(SEED); + long farSuper = UniverseScale.cellsForLightYears(home.radiusLy() * 4d) / cfg().minSpacing; + long farCluster = farSuper / gen.clusters().spacingSuperCells() + 1L; + assertFalse("a cluster turned up outside its own galaxy", + gen.clusters().clusterAtIndex(SEED, home, farCluster, 0L, 0L).isPresent()); + } + + // ─── What the fine lattice does to the star field ────────────────────────── + + @Test + public void aClusterHoldsFarMoreStarsThanTheFieldAroundit() { + // The point of the whole tier: the stratified lattice caps density at about three times the + // mean, while a real cluster runs tens of times the field. Measured as seats found in the same + // volume, inside a cluster and beside it. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg()); + Galaxy home = gen.galaxies().home(SEED); + ClusterField clusters = gen.clusters(); + long s = cfg().minSpacing; + + StarCluster found = null; + for (long cx = -6L; cx <= 6L && found == null; cx++) { + for (long cy = -4L; cy <= 4L && found == null; cy++) { + Optional c = clusters.clusterAtIndex(SEED, home, cx, cy, 0L); + if (c.isPresent() && c.get().subdivision() > 1) { + found = c.get(); + } + } + } + assertTrue("the sweep must find a cluster to measure", found != null); + + int inside = seatsInSuperCell(gen, found.centreSuperX(), found.centreSuperY(), + found.centreSuperZ(), s); + int outside = seatsInSuperCell(gen, found.centreSuperX() + 6L * found.radiusSuperCells(), + found.centreSuperY(), found.centreSuperZ(), s); + assertTrue("a cluster must be denser than the field beside it (" + inside + " vs " + outside + + ") for " + found, inside > outside); + assertTrue("and the field outside it must hold at most the one seat a coarse cell allows", + outside <= 1); + } + + @Test + public void aClusterCannotRefineACellBelowWhatASystemNeeds() { + // A cluster cannot conjure room its coarse cell never had. Refining below the smallest cell a + // system can be more than a lone star in would produce a field of bare stars, which is the + // opposite of a cluster — so a spacing too tight to refine simply is not refined, exactly as + // too tight a spacing already degenerates rather than erroring. + int tiny = 16; + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(new GalaxyGenConfig(tiny, 0.9d, + GalaxyGenConfig.DEFAULT_GALAXY_SPACING, GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, + null, null)); + assertTrue("a spacing of " + tiny + " cells is below the refinement floor", + tiny < UniverseScale.MIN_LATTICE_EDGE_CELLS * 2L); + + // At this spacing every seat must still attribute to its own COARSE super-cell, i.e. nothing + // was subdivided into cells too small to hold anything. + int checked = 0; + for (long sup = 0; sup < 40; sup++) { + Optional anchor = gen.anchorAt(SEED, + GalacticCoord.ofSectorLocal(sup * tiny, 0L, 0L, 0L, 0L, 0L)); + if (!anchor.isPresent()) { + continue; + } + assertEquals("a seat must stay in the coarse cell that was probed", sup, + Math.floorDiv(anchor.get().sectorX(), (long) tiny)); + checked++; + } + assertTrue(checked > 3); + } + + @Test + public void attributionNeverCrossesACoarseSuperCell() { + // The invariant that survives the refinement: whatever lattice is in force locally, a cell is + // attributed to a seat inside its OWN coarse super-cell. That is what keeps member attribution + // exact and two systems' neighbourhoods from interleaving. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg()); + long s = cfg().minSpacing; + int checked = 0; + for (long sup = -3L; sup <= 3L; sup++) { + for (long offset : new long[] {0L, s / 4L, s / 2L, s - 1L}) { + GalacticCoord probe = GalacticCoord.ofSectorLocal(sup * s + offset, 0L, 0L, 0L, 0L, 0L); + Optional anchor = gen.anchorAt(SEED, probe); + if (!anchor.isPresent()) { + continue; + } + assertEquals("attribution crossed a coarse super-cell face", sup, + Math.floorDiv(anchor.get().sectorX(), s)); + checked++; + } + } + assertTrue(checked > 3); + } + + /** + * Every seat inside ONE coarse super-cell, enumerated through the region query so the sub-lattice + * is walked the way the generator itself walks it. Counting probes along a line would miss every + * sub-cell off that line, and would read a refined cell as ordinary field. + */ + private static int seatsInSuperCell(ClusteredGalaxyGenerator gen, long supX, long supY, long supZ, + long s) { + return gen.systemsInRegion(SEED, + GalacticCoord.ofSectorLocal(supX * s, supY * s, supZ * s, 0L, 0L, 0L), + GalacticCoord.ofSectorLocal(supX * s + s - 1L, supY * s + s - 1L, supZ * s + s - 1L, + 0L, 0L, 0L)).size(); + } + +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java index fa6fa58af..454708696 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java @@ -16,6 +16,7 @@ import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.GalacticAnchor; import zmaster587.advancedRocketry.universe.EmptyGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.IGalaxyGenerator; @@ -395,17 +396,17 @@ public void systemsAreLocationAgnostic() { @Test public void anchorsDrainOnceThenPersistedStoreWins() { UniverseRegistry reg = new UniverseRegistry(); - Map anchors = new HashMap<>(); - anchors.put(1, GalacticCoord.ofSectorLocal(1, 0, 0, 0, 0, 0)); - anchors.put(2, GalacticCoord.ofSectorLocal(2, 0, 0, 0, 0, 0)); + Map anchors = new HashMap<>(); + anchors.put(1, GalacticAnchor.inHome(GalacticCoord.ofSectorLocal(1, 0, 0, 0, 0, 0))); + anchors.put(2, GalacticAnchor.inHome(GalacticCoord.ofSectorLocal(2, 0, 0, 0, 0, 0))); reg.applyAnchors(anchors, false); assertEquals(Optional.of(GalacticCoord.ofSectorLocal(1, 0, 0, 0, 0, 0)), reg.coordForSystem(1)); assertEquals(Optional.of(GalacticCoord.ofSectorLocal(2, 0, 0, 0, 0, 0)), reg.coordForSystem(2)); // Second drain with DIFFERENT anchors is a no-op (already seeded) unless a reset is forced. - Map moved = new HashMap<>(); - moved.put(1, GalacticCoord.ofSectorLocal(50, 0, 0, 0, 0, 0)); + Map moved = new HashMap<>(); + moved.put(1, GalacticAnchor.inHome(GalacticCoord.ofSectorLocal(50, 0, 0, 0, 0, 0))); reg.applyAnchors(moved, false); assertEquals("re-draining without reset must not move the anchor", Optional.of(GalacticCoord.ofSectorLocal(1, 0, 0, 0, 0, 0)), reg.coordForSystem(1)); @@ -426,8 +427,8 @@ public void anchorsSeededLatchPersistsThroughNbt() { round.readFromNBT(tag); // The latch survived, so a fresh anchor drain is ignored (persisted store wins across restarts). - Map anchors = new HashMap<>(); - anchors.put(3, GalacticCoord.ofSectorLocal(3, 0, 0, 0, 0, 0)); + Map anchors = new HashMap<>(); + anchors.put(3, GalacticAnchor.inHome(GalacticCoord.ofSectorLocal(3, 0, 0, 0, 0, 0))); round.applyAnchors(anchors, false); assertFalse("a restart must not re-seed anchors over the persisted store", round.coordForSystem(3).isPresent()); From 8c088fcb9c359e8a1eec288505d1d9f79b48e583 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Fri, 14 Aug 2026 18:59:50 +0300 Subject: [PATCH 18/42] feat: a star cluster carries the cloud it condensed out of - A nebula is derived from its cluster, not seated on a lattice of its own - Dark, emitting and reflecting are one age sequence, not three draws - A starless molecular cloud is a cluster that refines nothing - Expose how thick a cloud is at a point, with no consumer yet --- docs/README_PLANETDEFS.md | 11 + .../universe/ClusteredGalaxyGenerator.java | 11 + .../universe/GalaxyGenConfig.java | 24 ++- .../advancedRocketry/universe/Nebula.java | 166 +++++++++++++++ .../universe/NebulaField.java | 134 ++++++++++++ .../test/unit/NebulaTest.java | 194 ++++++++++++++++++ .../test/unit/StarClusterTest.java | 2 +- 7 files changed, 536 insertions(+), 6 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/Nebula.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java diff --git a/docs/README_PLANETDEFS.md b/docs/README_PLANETDEFS.md index e755e27a7..dae1d5655 100644 --- a/docs/README_PLANETDEFS.md +++ b/docs/README_PLANETDEFS.md @@ -136,6 +136,17 @@ consequence worth knowing: **the 10 000 AU separation floor is a property of the global constant.** Inside a cluster stars stand closer than a wide binary, and a system there keeps fewer outer bodies, by the same rule that applies everywhere else. +**Nebulae come with the clusters, not separately.** A molecular cloud, the young cluster condensing +out of it and the ancient cluster that has blown it away are one object at three ages — so a cloud is +derived from its cluster and how much gas that cluster's age has left, and its look (dark, emitting, +reflecting) is that same sequence rather than three separate options. A cloud with no stars in it yet +is a cluster type that refines nothing. Ancient globulars correctly have no cloud at all. + +A nebula is **diffuse matter, not a body**: it has no cell name, it is not a destination, and it may +freely overlap whatever it lies across — the same rule as a system's comet cloud, where attribution +reads names rather than matter. **It has no effect on anything yet**: what a cloud does to a ship that +flies into it is a separate decision and none of its numbers has been settled. + ### Where authored content goes — `galaxy` and `galacticCoord` A ``'s `galacticCoord` is **galaxy-local**: an offset in cells from the DECLARATION ORIGIN of the diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index b997bf92d..26ca8df81 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -176,12 +176,14 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { private final GalaxyGenConfig config; private final GalaxyField galaxies; private final ClusterField clusters; + private final NebulaField nebulae; private final long totalStarWeight; public ClusteredGalaxyGenerator(GalaxyGenConfig config) { this.config = (config == null) ? GalaxyGenConfig.defaults() : config; this.galaxies = new GalaxyField(this.config); this.clusters = new ClusterField(this.config); + this.nebulae = new NebulaField(this.config, this.clusters); long w = 0L; // accumulate in long so a few near-Integer.MAX weights cannot overflow the sum for (GalaxyGenConfig.StarType t : this.config.starTypes) { w += t.weight; @@ -203,6 +205,15 @@ public ClusterField clusters() { return clusters; } + /** + * The clouds those clusters are wrapped in. Diffuse matter, so it names nothing and places + * nothing — it is what makes a cluster visible from outside, and the seam any later consequence + * of flying into one would be written against. + */ + public NebulaField nebulae() { + return nebulae; + } + @Override public Optional systemAt(long seed, GalacticCoord coord) { Optional g = systemForLattice(seed, diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index af47c0638..feb283542 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -142,14 +142,24 @@ public static final class ClusterType { public final int subdivision; public final double minRadiusLy; public final double maxRadiusLy; + /** + * How much of its natal cloud a cluster of this type still has, {@code 0}..{@code 1} — which + * is the same thing as how OLD it is. An open cluster is young and still wrapped in gas; a + * globular is ancient and has none at all, which is why real globulars are gas-free. + * + *

It is the only input a nebula needs, and it is why a nebula is not seated separately: a + * cluster and its cloud are one object at two ages.

+ */ + public final double nebulaFraction; public final int weight; public ClusterType(String name, int subdivision, double minRadiusLy, double maxRadiusLy, - int weight) { + double nebulaFraction, int weight) { this.name = (name == null || name.isEmpty()) ? "CLUSTER" : name; this.subdivision = Math.max(1, subdivision); this.minRadiusLy = Math.max(0.01d, minRadiusLy); this.maxRadiusLy = Math.max(this.minRadiusLy, maxRadiusLy); + this.nebulaFraction = Math.min(1d, Math.max(0d, nebulaFraction)); this.weight = Math.max(1, weight); } } @@ -271,9 +281,13 @@ private static List defaultGalaxyTypes() { */ private static List defaultClusterTypes() { List l = new ArrayList<>(); - // name k radius band (ly) weight - l.add(new ClusterType("Open Cluster", 4, 5d, 15d, 80)); - l.add(new ClusterType("Globular Cluster", 14, 20d, 40d, 20)); + // name k radius band (ly) gas weight + // A molecular cloud is a cluster whose stars have not formed: it refines nothing (k = 1) and + // is all gas. That it drops out of the SAME table as the others is the point — a cloud, a + // young cluster and an ancient one are one sequence, not three features. + l.add(new ClusterType("Molecular Cloud", 1, 10d, 30d, 1.0d, 60)); + l.add(new ClusterType("Open Cluster", 4, 5d, 15d, 0.55d, 80)); + l.add(new ClusterType("Globular Cluster", 14, 20d, 40d, 0d, 20)); return Collections.unmodifiableList(l); } @@ -281,7 +295,7 @@ private static List defaultClusterTypes() { * The cluster every galaxy has at its own centre — the richest one, and no special case: it is a * cluster like the others, drawn at the galaxy's centre instead of on the cluster lattice. */ - public static final ClusterType NUCLEUS = new ClusterType("Nucleus", 25, 4d, 8d, 1); + public static final ClusterType NUCLEUS = new ClusterType("Nucleus", 25, 4d, 8d, 0.4d, 1); /** Edge of the cube that holds at most one cluster, in light years. */ public static final double CLUSTER_SPACING_LY = 300d; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java b/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java new file mode 100644 index 000000000..9c363756f --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java @@ -0,0 +1,166 @@ +package zmaster587.advancedRocketry.universe; + +/** + * A nebula: the diffuse cloud a star cluster is wrapped in. + * + *

It is not seated separately, and that is the design. A molecular cloud, the young cluster + * that condenses out of it and the ancient cluster that has blown it away are ONE object at three + * ages — so a nebula is derived from a {@link StarCluster} and its type's residual gas, and there is + * no second lattice, no second spacing number and no way for a cloud and its cluster to disagree + * about where they are.

+ * + *

What it is for today, and what it is a seam for

+ *

Today a cluster is a pure refinement of the star lattice: it has no property anything outside it + * can observe, so it can only be discovered by counting stars. A nebula is what makes a cluster a + * LANDMARK rather than a statistical fact.

+ * + *

{@link #densityAt} is the whole seam. Every consequence a nebula could ever have — a + * sensor it muffles, a drag it imposes, something it conceals, something a ship mines out of it — is a + * function of how thick it is at a point. That function exists now and is tested; what does NOT exist + * is any consumer of it, deliberately: none of those numbers is ratified, and inventing them + * alongside the thing they judge is how a mechanic ends up measuring itself.

+ * + *

Diffuse matter is NOT a body

+ *

A nebula has no cell name, is not a destination, and does not participate in one-real-body-per- + * cell. It is the same category as a system's comet cloud: attribution reads names, not matter, + * so a nebula may freely overlap whatever it lies across.

+ * + *

Immutable value type.

+ */ +public final class Nebula { + + /** + * How much wider than its cluster a nebula reaches. Real clouds are far larger than the cluster + * inside them — Orion is about twelve light years across around a cluster of one. + */ + private static final double MIN_SPREAD = 1.5d; + private static final double MAX_SPREAD = 3d; + + /** Below this much residual gas a cluster has no cloud left worth drawing. */ + static final double MINIMUM_VISIBLE_GAS = 0.05d; + + /** + * What a nebula looks like — DERIVED from how much gas is left, never drawn, because the three + * appearances are one age sequence and not three options. + * + *

Youngest first: the cloud is dark and molecular while its stars are still forming inside it; + * once they are burning, the hottest of them ionise what is left and it emits; once the gas is + * blown clear, the remaining dust merely reflects.

+ */ + public enum Appearance { + /** Thick and cold: it blocks the light behind it rather than making any of its own. */ + DARK, + /** Ionised by the stars inside it, and shining because of them. */ + EMISSION, + /** Thin dust, lit by whatever is nearby. */ + REFLECTION + } + + private final StarCluster cluster; + private final Appearance appearance; + private final double centreXLy; + private final double centreYLy; + private final double centreZLy; + private final double radiusLy; + private final double peakDensity; + + public Nebula(StarCluster cluster, Appearance appearance, double centreXLy, double centreYLy, + double centreZLy, double radiusLy, double peakDensity) { + this.cluster = cluster; + this.appearance = appearance; + this.centreXLy = centreXLy; + this.centreYLy = centreYLy; + this.centreZLy = centreZLy; + this.radiusLy = Math.max(0.01d, radiusLy); + this.peakDensity = Math.min(1d, Math.max(0d, peakDensity)); + } + + /** The cluster this cloud belongs to — the same object at a different age. */ + public StarCluster cluster() { + return cluster; + } + + public Appearance appearance() { + return appearance; + } + + /** Its centre in light years, in the static frame. It shares its cluster's. */ + public double centreXLy() { + return centreXLy; + } + + public double centreYLy() { + return centreYLy; + } + + public double centreZLy() { + return centreZLy; + } + + /** How far it reaches, in light years. Wider than the cluster inside it. */ + public double radiusLy() { + return radiusLy; + } + + /** How thick it is at its densest, {@code 0}..{@code 1}. */ + public double peakDensity() { + return peakDensity; + } + + /** + * How thick this nebula is at a point, {@code 0}..{@code 1} — zero outside its radius. + * + *

This is the seam. A Gaussian falloff, so a cloud has no edge to see: it thins out, + * which is what diffuse matter does and what any consequence built on it will want. The + * appearance decides how it is drawn; this decides how much of it there is.

+ */ + public double densityAt(double xLy, double yLy, double zLy) { + double dx = xLy - centreXLy; + double dy = yLy - centreYLy; + double dz = zLy - centreZLy; + double rSq = dx * dx + dy * dy + dz * dz; + if (rSq > radiusLy * radiusLy) { + return 0d; + } + double scale = radiusLy / 2d; + return peakDensity * Math.exp(-rSq / (scale * scale)); + } + + /** The same reading at a cell name — the form the rest of the layer asks in. */ + public double densityAtSector(long sectorX, long sectorY, long sectorZ) { + return densityAt(UniverseScale.lightYearsForCells(sectorX), + UniverseScale.lightYearsForCells(sectorY), + UniverseScale.lightYearsForCells(sectorZ)); + } + + /** Whether a point is inside this nebula at all. */ + public boolean contains(double xLy, double yLy, double zLy) { + double dx = xLy - centreXLy; + double dy = yLy - centreYLy; + double dz = zLy - centreZLy; + return dx * dx + dy * dy + dz * dz <= radiusLy * radiusLy; + } + + /** + * How wide a cloud of {@code spread} reaches around a cluster of {@code clusterRadiusLy}, and how + * dense it is at its centre, given the residual gas. Static so the seating code and a test can + * agree without one of them re-deriving it. + */ + static double spreadFor(double fraction) { + return MIN_SPREAD + Math.min(1d, Math.max(0d, fraction)) * (MAX_SPREAD - MIN_SPREAD); + } + + /** The appearance a cloud with this much gas left has. One number, three ages, in order. */ + static Appearance appearanceFor(double fraction) { + if (fraction >= 0.7d) { + return Appearance.DARK; + } + return fraction >= 0.3d ? Appearance.EMISSION : Appearance.REFLECTION; + } + + @Override + public String toString() { + return "Nebula[" + appearance + " r=" + (long) radiusLy + "ly d=" + peakDensity + + " around " + cluster + "]"; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java b/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java new file mode 100644 index 000000000..aaa863564 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java @@ -0,0 +1,134 @@ +package zmaster587.advancedRocketry.universe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Where the nebulae are — which is: wherever a star cluster still has gas. + * + *

There is no nebula lattice and no nebula spacing. A cloud is derived from the cluster it wraps + * and that cluster's residual gas, so the two can never disagree about where they are, and adding + * nebulae cost the generator no new partition, no new occupancy draw and no new number to invent. + * A cloud with no stars in it is expressible too — it is a cluster type whose subdivision is 1.

+ * + *

This class is a SEAM, and it has no consumer yet

+ *

What a nebula DOES to a ship that flies into it — muffled sensors, drag, concealment, something + * to mine — is deliberately not here. None of those numbers is ratified, and building a mechanic + * beside the criteria that would judge it is how a mechanic comes to measure itself. What is here is + * everything such a mechanic would need: where the clouds are, how big they are, and + * {@link Nebula#densityAt} for how thick one is at a point.

+ */ +public final class NebulaField { + + private static final long SALT_NEBULA_GAS = 0x301L; + private static final long SALT_NEBULA_SPREAD = 0x302L; + + /** How much a cluster's residual gas may vary from the figure its type states. */ + private static final double GAS_VARIATION = 0.35d; + + private final GalaxyGenConfig config; + private final ClusterField clusters; + + public NebulaField(GalaxyGenConfig config, ClusterField clusters) { + this.config = (config == null) ? GalaxyGenConfig.defaults() : config; + this.clusters = clusters; + } + + /** + * The cloud wrapping this cluster, or empty when it has none left. + * + *

An ancient globular has blown its gas away and gets nothing; a molecular cloud is all gas and + * no stars; the open clusters between them are the interesting middle.

+ */ + public Optional nebulaOf(long seed, StarCluster cluster) { + if (cluster == null) { + return Optional.empty(); + } + double stated = cluster.type().nebulaFraction; + if (!(stated > 0d)) { + return Optional.empty(); + } + // The type says how gassy its age is; the draw says how gassy THIS one is. + double swing = (CellHash.of(seed, cluster.centreSuperX(), cluster.centreSuperY(), + cluster.centreSuperZ(), SALT_NEBULA_GAS) >>> 11) * 0x1.0p-53; + double gas = Math.min(1d, Math.max(0d, stated + (swing - 0.5d) * 2d * GAS_VARIATION)); + if (gas < Nebula.MINIMUM_VISIBLE_GAS) { + return Optional.empty(); + } + + double spreadRoll = CellHash.norm(CellHash.of(seed, cluster.centreSuperX(), + cluster.centreSuperY(), cluster.centreSuperZ(), SALT_NEBULA_SPREAD)); + double clusterRadiusLy = UniverseScale.lightYearsForCells( + (double) cluster.radiusSuperCells() * config.minSpacing); + double radiusLy = clusterRadiusLy * Nebula.spreadFor(spreadRoll); + + long s = config.minSpacing; + return Optional.of(new Nebula(cluster, Nebula.appearanceFor(gas), + UniverseScale.lightYearsForCells((double) cluster.centreSuperX() * s), + UniverseScale.lightYearsForCells((double) cluster.centreSuperY() * s), + UniverseScale.lightYearsForCells((double) cluster.centreSuperZ() * s), + radiusLy, gas)); + } + + /** The cloud covering this coarse super-cell, if a cluster covers it and still has one. */ + public Optional nebulaAt(long seed, Galaxy galaxy, long supX, long supY, long supZ) { + Optional cluster = clusters.clusterAt(seed, galaxy, supX, supY, supZ); + return cluster.isPresent() ? nebulaOf(seed, cluster.get()) : Optional.empty(); + } + + /** + * Every nebula seated in the box of coarse super-cells {@code [min, max]} — what a render or a + * long-range scan asks, because a cloud is meant to be seen from OUTSIDE it. + * + *

Enumerated over the CLUSTER lattice rather than per super-cell, so the cost is the number of + * cluster cells the box crosses and not its volume.

+ */ + public List nebulaeInRegion(long seed, Galaxy galaxy, long supMinX, long supMinY, + long supMinZ, long supMaxX, long supMaxY, long supMaxZ) { + List out = new ArrayList<>(); + if (galaxy == null) { + return out; + } + long spacing = clusters.spacingSuperCells(); + // A cloud reaches beyond its own cluster cell, so the sweep widens by one cell each way. + for (long cx = Math.floorDiv(supMinX, spacing) - 1L; + cx <= Math.floorDiv(supMaxX, spacing) + 1L; cx++) { + for (long cy = Math.floorDiv(supMinY, spacing) - 1L; + cy <= Math.floorDiv(supMaxY, spacing) + 1L; cy++) { + for (long cz = Math.floorDiv(supMinZ, spacing) - 1L; + cz <= Math.floorDiv(supMaxZ, spacing) + 1L; cz++) { + Optional cluster = clusters.clusterAtIndex(seed, galaxy, cx, cy, cz); + if (!cluster.isPresent()) { + continue; + } + Optional nebula = nebulaOf(seed, cluster.get()); + if (nebula.isPresent()) { + out.add(nebula.get()); + } + } + } + } + // The nucleus is not on the cluster lattice, so it is asked for separately — the same + // exception the cluster tier already makes for it. + Optional nucleus = clusters.nucleusOf(seed, galaxy); + if (nucleus.isPresent()) { + Optional core = nebulaOf(seed, nucleus.get()); + if (core.isPresent()) { + out.add(core.get()); + } + } + return out; + } + + /** + * How much diffuse matter lies at this cell, {@code 0}..{@code 1} — the one query a consequence + * would be written against, whatever the consequence turns out to be. + */ + public double densityAtSector(long seed, Galaxy galaxy, long sectorX, long sectorY, long sectorZ) { + long s = config.minSpacing; + Optional nebula = nebulaAt(seed, galaxy, Math.floorDiv(sectorX, s), + Math.floorDiv(sectorY, s), Math.floorDiv(sectorZ, s)); + return nebula.isPresent() ? nebula.get().densityAtSector(sectorX, sectorY, sectorZ) : 0d; + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java new file mode 100644 index 000000000..126766454 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java @@ -0,0 +1,194 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import java.util.List; +import java.util.Optional; + +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Galaxy; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.Nebula; +import zmaster587.advancedRocketry.universe.NebulaField; +import zmaster587.advancedRocketry.universe.StarCluster; +import zmaster587.advancedRocketry.universe.UniverseScale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for nebulae — the diffuse cloud a star cluster is wrapped in. + * + *

What is pinned is that a cloud is DERIVED from its cluster and cannot disagree with it, that its + * appearance is one age sequence rather than three drawn options, that it reaches beyond the cluster + * inside it (so it can be seen from outside, which is the whole point of having it), and that + * {@code densityAt} is a continuous falloff with no edge — because that function is the seam every + * later consequence will be written against.

+ * + *

There is deliberately no test of what a nebula DOES, because it does nothing yet. None of + * those numbers is ratified.

+ */ +public class NebulaTest { + + private static final long SEED = 0xC10DDL; + + private static ClusteredGalaxyGenerator gen() { + return new ClusteredGalaxyGenerator(GalaxyGenConfig.defaults()); + } + + private static StarCluster clusterOfType(GalaxyGenConfig.ClusterType type) { + return new StarCluster(type, 100L, 0L, 0L, 2L); + } + + private static GalaxyGenConfig.ClusterType typeWithGas(double gas) { + return new GalaxyGenConfig.ClusterType("Test", 4, 5d, 15d, gas, 1); + } + + // ─── The derivation ──────────────────────────────────────────────────────── + + @Test + public void aCloudBelongsToItsClusterAndSharesItsPlace() { + // Not seated separately, and that is the design: a cloud and the cluster inside it are one + // object at two ages, so there is no way for them to disagree about where they are. + NebulaField field = gen().nebulae(); + StarCluster cluster = clusterOfType(typeWithGas(0.8d)); + Optional nebula = field.nebulaOf(SEED, cluster); + assertTrue(nebula.isPresent()); + assertEquals(cluster, nebula.get().cluster()); + + double expectedX = UniverseScale.lightYearsForCells( + (double) cluster.centreSuperX() * GalaxyGenConfig.DEFAULT_MIN_SPACING); + assertEquals("a cloud is centred on its cluster", expectedX, nebula.get().centreXLy(), 1e-6d); + } + + @Test + public void aClusterWithNoGasLeftHasNoCloud() { + // An ancient globular has blown its gas away — real ones are gas-free, and that is what the + // type table states rather than something the generator decides separately. + NebulaField field = gen().nebulae(); + assertFalse(field.nebulaOf(SEED, clusterOfType(typeWithGas(0d))).isPresent()); + } + + @Test + public void aCloudReachesBeyondTheClusterInsideIt() { + // The point of having one: a cluster is otherwise a pure refinement of the lattice with no + // property anything outside it can observe. Real clouds dwarf their clusters. + NebulaField field = gen().nebulae(); + StarCluster cluster = clusterOfType(typeWithGas(0.9d)); + Nebula nebula = field.nebulaOf(SEED, cluster).get(); + double clusterRadiusLy = UniverseScale.lightYearsForCells( + (double) cluster.radiusSuperCells() * GalaxyGenConfig.DEFAULT_MIN_SPACING); + assertTrue("a cloud of " + nebula.radiusLy() + " ly must exceed its cluster's " + + clusterRadiusLy, nebula.radiusLy() > clusterRadiusLy); + } + + @Test + public void appearanceIsAnAgeSequenceNotAChoice() { + // One number, three appearances, IN ORDER: dark while the stars are still forming inside it, + // emitting once they ionise what is left, reflecting once the gas is blown clear. Three + // independent draws would let a nearly-gone cloud come out thick and black. + // + // Pinned as the ORDERING over a sample rather than by repeating the thresholds here — a test + // that copies the derivation it is checking cannot fail when the derivation is wrong. + NebulaField field = gen().nebulae(); + double darkestEmission = 0d; + double thinnestDark = 1d; + double darkestReflection = 0d; + double thinnestEmission = 1d; + int seen = 0; + for (int i = 0; i <= 20; i++) { + double stated = i / 20d; + Optional n = field.nebulaOf(SEED + i, clusterOfType(typeWithGas(stated))); + if (!n.isPresent()) { + continue; + } + double gas = n.get().peakDensity(); + seen++; + switch (n.get().appearance()) { + case DARK: + thinnestDark = Math.min(thinnestDark, gas); + break; + case EMISSION: + darkestEmission = Math.max(darkestEmission, gas); + thinnestEmission = Math.min(thinnestEmission, gas); + break; + default: + darkestReflection = Math.max(darkestReflection, gas); + break; + } + } + assertTrue("the sample must contain clouds", seen > 5); + assertTrue("every DARK cloud must be thicker than every EMISSION one", + thinnestDark > darkestEmission); + assertTrue("and every EMISSION one thicker than every REFLECTION one", + thinnestEmission > darkestReflection); + } + + // ─── The seam ────────────────────────────────────────────────────────────── + + @Test + public void densityFallsOffSmoothlyAndStopsAtTheRadius() { + // THE seam: every consequence a nebula could have — a muffled sensor, a drag, something + // concealed, something mined — is a function of how thick it is here. Diffuse matter has no + // edge, so the falloff is continuous; what it does have is a bound, so a consumer can stop. + NebulaField field = gen().nebulae(); + Nebula n = field.nebulaOf(SEED, clusterOfType(typeWithGas(0.8d))).get(); + double cx = n.centreXLy(); + double cy = n.centreYLy(); + double cz = n.centreZLy(); + + double centre = n.densityAt(cx, cy, cz); + double half = n.densityAt(cx + n.radiusLy() * 0.5d, cy, cz); + double edge = n.densityAt(cx + n.radiusLy() * 0.99d, cy, cz); + assertEquals("the centre must be the stated peak", n.peakDensity(), centre, 1e-9d); + assertTrue("it must thin outwards", centre > half); + assertTrue("and keep thinning", half > edge); + assertTrue("but never reach zero inside its own radius", edge > 0d); + assertEquals("and be exactly zero outside it", 0d, + n.densityAt(cx + n.radiusLy() * 1.01d, cy, cz), 0d); + assertTrue(n.contains(cx, cy, cz)); + assertFalse(n.contains(cx + n.radiusLy() * 1.01d, cy, cz)); + } + + @Test + public void aSectorReadingAgreesWithTheLengthItStandsFor() { + // The rest of the layer asks in cell names; a nebula is written in light years. If those two + // disagreed, a cloud would be placed by one metric and read by another. + NebulaField field = gen().nebulae(); + Nebula n = field.nebulaOf(SEED, clusterOfType(typeWithGas(0.8d))).get(); + long sector = UniverseScale.cellsAt(n.centreXLy()); + assertEquals(n.densityAt(UniverseScale.lightYearsForCells(sector), 0d, 0d), + n.densityAtSector(sector, 0L, 0L), 1e-9d); + } + + // ─── Where they turn up ──────────────────────────────────────────────────── + + @Test + public void aGalaxyHoldsNebulaeAndTheyAreDeterministic() { + ClusteredGalaxyGenerator g = gen(); + Galaxy home = g.galaxies().home(SEED); + long spacing = g.clusters().spacingSuperCells(); + List found = g.nebulae().nebulaeInRegion(SEED, home, -3L * spacing, -2L * spacing, + -2L * spacing, 3L * spacing, 2L * spacing, 2L * spacing); + assertFalse("a galaxy must hold clouds", found.isEmpty()); + assertEquals("the same query must answer the same way", found.size(), + g.nebulae().nebulaeInRegion(SEED, home, -3L * spacing, -2L * spacing, -2L * spacing, + 3L * spacing, 2L * spacing, 2L * spacing).size()); + for (Nebula n : found) { + assertNotNull(n.appearance()); + assertTrue("a cloud that exists must have something in it", n.peakDensity() > 0d); + } + } + + @Test + public void thereIsNoDiffuseMatterOutsideAGalaxy() { + // A cloud only exists where a cluster does, and a cluster only exists inside a galaxy — one + // chain, not a separate rule about the void. + ClusteredGalaxyGenerator g = gen(); + Galaxy home = g.galaxies().home(SEED); + long farSector = UniverseScale.cellsForLightYears(home.radiusLy() * 4d); + assertEquals(0d, g.nebulae().densityAtSector(SEED, home, farSector, 0L, 0L), 0d); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java index d5b5707f5..12f67d981 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java @@ -34,7 +34,7 @@ private static GalaxyGenConfig cfg() { } private static GalaxyGenConfig.ClusterType type(int k) { - return new GalaxyGenConfig.ClusterType("Test", k, 5d, 15d, 1); + return new GalaxyGenConfig.ClusterType("Test", k, 5d, 15d, 0.5d, 1); } // ─── The commensurate construction ───────────────────────────────────────── From cf4927e0852ed69b99e300ae0f90627a259211ee Mon Sep 17 00:00:00 2001 From: StannisMod Date: Sat, 15 Aug 2026 14:47:28 +0300 Subject: [PATCH 19/42] fix: a telescope resolves the system that owns a cell, and its reach is a length - resolve a look through the owning anchor, not a seat query - stride a survey by one star's territory; the local radar keeps cells - state the reach in light years, derive the aim in steps - show the aim's length in the GUI, computed server-side --- .../advancedRocketry/api/ARConfiguration.java | 20 +- .../command/test/TestProbeCommand.java | 30 ++- .../tile/multiblock/TileObservatory.java | 58 +++-- .../advancedRocketry/universe/RegionScan.java | 236 ++++++++++++------ .../universe/TelescopeScan.java | 28 ++- .../assets/advancedrocketry/lang/en_US.lang | 5 +- .../assets/advancedrocketry/lang/ru_RU.lang | 5 +- .../client/MachineGuiClientGroupE2ETest.java | 18 +- .../server/TelescopeRegionScanE2ETest.java | 92 ++++++- .../test/unit/TelescopeRegionScanTest.java | 177 +++++++++++-- 10 files changed, 510 insertions(+), 159 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java b/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java index 97b837224..cc96f5e6f 100644 --- a/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java +++ b/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java @@ -287,19 +287,19 @@ public class ARConfiguration { @ConfigProperty public int planetDiscoveryChance; @ConfigProperty - public int telescopeScanRangeSectors; + public double telescopeScanRangeLightYears; @ConfigProperty - public int telescopeScanHalfWidthSectors; + public int telescopeScanHalfWidthSteps; @ConfigProperty - public int telescopeScanMaxSectors; + public int telescopeScanMaxCells; @ConfigProperty public int telescopeScanBaseTicks; @ConfigProperty - public int telescopeScanTicksPerSector; + public double telescopeScanTicksPerLightYear; @ConfigProperty public int telescopeScanCellsPerStep; @ConfigProperty - public int telescopePassiveRadiusSectors; + public int telescopePassiveRadiusCells; @ConfigProperty public int telescopeSurveyDataPerStep; @ConfigProperty @@ -535,14 +535,14 @@ public static void loadPreInit() { //Planet arConfig.planetsMustBeDiscovered = config.get(PLANET, "planetsMustBeDiscovered", false, "Planets must be discovered in the warp controller before being visible").getBoolean(); arConfig.planetDiscoveryChance = config.get(PLANET, "planetDiscoveryChance", 5, "Chance of planet discovery in the warp controller, chance is 1/n", 1, Integer.MAX_VALUE).getInt(); - arConfig.telescopeScanRangeSectors = config.get(PLANET, "telescopeScanRangeSectors", 24, "How far, in galactic sectors, an observatory's region scan can be aimed. This is the instrument's horizon: beyond it the sky is not resolvable, which is what keeps an endless universe from being read off a telescope. A scan aimed farther is clamped to this.", 1, Integer.MAX_VALUE).getInt(); - arConfig.telescopeScanHalfWidthSectors = config.get(PLANET, "telescopeScanHalfWidthSectors", 2, "Half-width, in sectors, of the region one survey sweeps. 0 means a single sector, 1 a 3x3x3 neighbourhood, 2 a 5x5x5, and so on. Narrowed automatically when the resulting region would exceed telescopeScanMaxSectors.", 0, Integer.MAX_VALUE).getInt(); - arConfig.telescopeScanMaxSectors = config.get(PLANET, "telescopeScanMaxSectors", 1000, "Hard ceiling on how many sectors one survey may cover. The width above is narrowed until the region fits under this. A sweep may be long, but never unbounded.", 1, Integer.MAX_VALUE).getInt(); + arConfig.telescopeScanRangeLightYears = config.get(PLANET, "telescopeScanRangeLightYears", 100d, "How far, in LIGHT YEARS, an observatory's region scan can be aimed. This is the instrument's horizon: beyond it the sky is not resolvable, which is what keeps an endless universe from being read off a telescope. A scan aimed farther is clamped to this. An operator aims in STEPS, and one step is one star's territory (the mean distance to a neighbouring star), so this reach divided by that spacing is how many steps out he may point it.", 0d, Double.MAX_VALUE).getDouble(); + arConfig.telescopeScanHalfWidthSteps = config.get(PLANET, "telescopeScanHalfWidthSteps", 2, "Half-width, in STEPS, of the region one survey sweeps - one step being one star's territory, the same stride the sweep walks by. 0 means a single look, 1 a 3x3x3 patch of neighbouring territories, 2 a 5x5x5, and so on. Narrowed automatically when the resulting region would exceed telescopeScanMaxCells.", 0, Integer.MAX_VALUE).getInt(); + arConfig.telescopeScanMaxCells = config.get(PLANET, "telescopeScanMaxCells", 1000, "Hard ceiling on how many cells one survey may LOOK AT (one per step, not one per cell of sky crossed). The width above is narrowed until the region fits under this. A sweep may be long, but never unbounded.", 1, Integer.MAX_VALUE).getInt(); arConfig.telescopeScanBaseTicks = config.get(PLANET, "telescopeScanBaseTicks", 200, "Ticks one STEP of a survey takes before distance is counted - the cost of holding the instrument on a patch of sky at all. Only applies with planetsMustBeDiscovered on; without research, an observation is instant.", 0, Integer.MAX_VALUE).getInt(); - arConfig.telescopeScanTicksPerSector = config.get(PLANET, "telescopeScanTicksPerSector", 100, "Extra ticks per sector of distance, per step. This is what makes a far region a longer survey than a near one.", 0, Integer.MAX_VALUE).getInt(); + arConfig.telescopeScanTicksPerLightYear = config.get(PLANET, "telescopeScanTicksPerLightYear", 20d, "Extra ticks per light year of distance, per step. This is what makes a far region a longer survey than a near one.", 0d, Double.MAX_VALUE).getDouble(); arConfig.telescopeScanCellsPerStep = config.get(PLANET, "telescopeScanCellsPerStep", 5, "How many cells of the region one step of a survey resolves. This is the bound that stops a sweep from enumerating everything at once.", 1, Integer.MAX_VALUE).getInt(); arConfig.telescopeSurveyDataPerStep = config.get(PLANET, "telescopeSurveyDataPerStep", 0, "Distance data one step of a survey consumes, drawn from the observatory's data buses the same way its asteroid scan draws. A step with too little data waits rather than resolving, so an unfed instrument stalls instead of working for free. Zero (the default) means a survey costs nothing - what it should cost is a balance question, not a mechanic one.", 0, Integer.MAX_VALUE).getInt(); - arConfig.telescopePassiveRadiusSectors = config.get(PLANET, "telescopePassiveRadiusSectors", 2, "How far, in sectors, the passive local radar reaches around the observatory's own cell. Passive is the mode that costs nothing and watches the neighbourhood; the directed survey is what looks far away.", 0, Integer.MAX_VALUE).getInt(); + arConfig.telescopePassiveRadiusCells = config.get(PLANET, "telescopePassiveRadiusCells", 2, "How far, in CELLS, the passive local radar reaches around the observatory's own cell. Cells and not star territories: this mode watches the neighbourhood, where the planet in the next cell over is a different destination from its star. Passive costs nothing; the directed survey is what looks far away.", 0, Integer.MAX_VALUE).getInt(); DimensionManager.dimOffset = config.getInt("minDimension", PLANET, 2, -127, 8000, "Lowest dimension ID that can be used for planets."); arConfig.canPlayerRespawnInSpace = config.get(PLANET, "allowPlanetRespawn", false, "Allow bed respawn on planets with breathable air.").getBoolean(); arConfig.forcePlayerRespawnInSpace = config.get(PLANET, "forcePlanetRespawn", false, "Allow bed respawn on planets even without breathable air. Requires 'allowPlanetRespawn=true'.").getBoolean(); diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 14d688497..ccbedac08 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -2610,15 +2610,21 @@ private void handleTelescope(MinecraftServer server, ICommandSender sender, Stri } /** - * The scan half of a telescope reply. The sector COUNT ships with the corners it was computed + * The scan half of a telescope reply. The cell COUNT ships with the corners it was computed * from, and the deadline with the clock it is measured against, so a stuck number says which * component is stuck. {@code side} is stated because every field here is the server's answer. + * + *

The instrument's HORIZON ships in both of its forms — the configured length and the number + * of steps it buys at this world's star spacing — because a reach that means nothing is a defect + * that reads as an empty sky, and no count alone can be checked against a telescope.

*/ private String telescopeScanFields(zmaster587.advancedRocketry.tile.multiblock.TileObservatory scope, zmaster587.advancedRocketry.universe.RegionScan scan, net.minecraft.world.WorldServer world) { long now = world.getTotalWorldTime(); zmaster587.advancedRocketry.space.GalacticCoord origin = scope.scanOrigin(); + zmaster587.advancedRocketry.universe.RegionScan.Tuning tuning = + zmaster587.advancedRocketry.universe.RegionScan.Tuning.fromConfig(); StringBuilder out = new StringBuilder(); out.append(",\"side\":\"server\",\"now\":").append(now) .append(",\"origin\":").append(origin == null ? "null" : "\"" + origin.cellKey() + "\"") @@ -2630,6 +2636,12 @@ private String telescopeScanFields(zmaster587.advancedRocketry.tile.multiblock.T // running scan is already looking at, below. .append(",\"aim\":").append(scope.scanDirectionIndex()) .append(",\"aimDistance\":").append(scope.getScanDistance()) + .append(",\"aimLy\":").append(scope.getAimLightYears()) + .append(",\"reachLy\":").append(tuning.maxRangeLightYears()) + .append(",\"reachSteps\":").append(tuning.maxRangeSteps()) + // What ONE step of that aim is worth in cells — the instrument's own stride, readable + // while it is idle, so a fixture can be placed where the next look will actually land. + .append(",\"stepCells\":").append(tuning.strideCells()) .append(",\"passive\":").append(scope.isPassive()); if (scan != null) { // The cell counts ship beside the region they are counted over, and the next deadline @@ -2640,7 +2652,11 @@ private String telescopeScanFields(zmaster587.advancedRocketry.tile.multiblock.T .append("\",\"cells\":").append(scan.totalCells()) .append(",\"cellsDone\":").append(scan.cellsDone()) .append(",\"cellsPerStep\":").append(scan.cellsPerStep()) - .append(",\"distance\":").append(scan.distanceSectors()) + // The reach in BOTH forms, and the stride that relates them: a survey that + // resolves nothing must be able to say whether it is looking at the wrong scale. + .append(",\"distance\":").append(scan.distanceCells()) + .append(",\"distanceLy\":").append(scan.distanceLightYears()) + .append(",\"stride\":").append(scan.strideCells()) .append(",\"start\":").append(scan.startTick()) .append(",\"stepDeadline\":").append(scan.stepDeadline()) .append(",\"ticksPerStep\":").append(scan.ticksPerStep()) @@ -10992,13 +11008,13 @@ private void handleMachineTickUntil(MinecraftServer server, ICommandSender sende // The telescope's reach and what a look costs in time, all read at scan START, // so flipping them at runtime is enough to exercise a short scan in a test // without waiting out a production-length observation. - "telescopeScanRangeSectors", - "telescopeScanHalfWidthSectors", - "telescopeScanMaxSectors", + "telescopeScanRangeLightYears", + "telescopeScanHalfWidthSteps", + "telescopeScanMaxCells", "telescopeScanBaseTicks", - "telescopeScanTicksPerSector", + "telescopeScanTicksPerLightYear", "telescopeScanCellsPerStep", - "telescopePassiveRadiusSectors", + "telescopePassiveRadiusCells", "telescopeSurveyDataPerStep", // The research master switch. A survey is instant without it and paced by the // time curve with it, so both halves of boundary B need it flippable at runtime. diff --git a/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java b/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java index e5cd4daaf..346cf061d 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java @@ -141,8 +141,14 @@ public class TileObservatory extends TileMultiPowerConsumer implements IModularI private int lastScanDiscoveries; /** Which way the operator has the instrument pointed, as an index into {@link #SCAN_DIRECTIONS}. */ private int scanDirection; - /** How far out, in sectors, he has it aimed. Clamped to the configured reach when it is used. */ + /** How far out, in STEPS, he has it aimed. Clamped to the configured reach when it is used. */ private int scanDistance = 1; + /** + * What one step of aim is worth in light years — how the operator's pick is turned into a length + * he can recognise. Derived from the SERVER's galaxy generator and synced, never computed on the + * client: a client attached to a pack whose star spacing it does not hold would quote its own. + */ + private double stepLightYears; /** Client-side only: which way the distance button just pressed wants to move the aim. */ private int pendingDistanceDelta; /** Watching the neighbourhood rather than a distant patch. The two modes are exclusive. */ @@ -299,6 +305,12 @@ public void update() { } if (!world.isRemote) { + // Once per load: the stride is the installed generator's, and that is fixed for a world. + if (stepLightYears <= 0d) { + stepLightYears = zmaster587.advancedRocketry.universe.UniverseScale + .lightYearsForCells(RegionScan.Tuning.fromConfig().strideCells()); + markDirty(); + } completeRegionScanIfDue(); } @@ -392,6 +404,7 @@ protected void writeNetworkData(NBTTagCompound nbt) { nbt.setInteger("lastScanDiscoveries", lastScanDiscoveries); nbt.setInteger("scanDirection", scanDirection); nbt.setInteger("scanDistance", scanDistance); + nbt.setDouble("scanStepLy", stepLightYears); nbt.setBoolean("scanPassive", passive); } @@ -414,6 +427,7 @@ protected void readNetworkData(NBTTagCompound nbt) { lastScanDiscoveries = nbt.getInteger("lastScanDiscoveries"); scanDirection = nbt.getInteger("scanDirection"); scanDistance = Math.max(1, nbt.getInteger("scanDistance")); + stepLightYears = nbt.getDouble("scanStepLy"); passive = nbt.getBoolean("scanPassive"); if (world != null && world.isRemote && prevSeed != lastSeed) { @@ -678,7 +692,7 @@ public List getModules(int ID, EntityPlayer player) { modules.add(new ModuleText(8, 70, LibVulpes.proxy.getLocalizedString("msg.observetory.scan.distance") - + " " + scanDistance, 0x2d2d2d, false)); + + " " + scanDistance + aimInLightYears(), 0x2d2d2d, false)); modules.add(new ModuleButton(100, 66, 4, "-", this, zmaster587.libVulpes.inventory.TextureResources.buttonBuild, LibVulpes.proxy.getLocalizedString("msg.observetory.scan.distance.tooltip"), 18, 18)); @@ -762,13 +776,14 @@ public int getMaxDistance() { /** * Aim the instrument at a region and start looking. Server side; one observation at a time. * - *

The distance is in galactic sectors and is clamped to the configured reach rather than - * refused — asking to see farther than the instrument can gets you the instrument's reach.

+ *

The distance is in STEPS — one step is one star's territory — and is clamped to the + * configured reach rather than refused: asking to see farther than the instrument can gets you + * the instrument's reach.

* * @return {@code false} when the machine is already looking somewhere, or when it does not know * where it is standing and so has nothing to aim FROM */ - public boolean beginRegionScan(int dirX, int dirY, int dirZ, int distanceSectors) { + public boolean beginRegionScan(int dirX, int dirY, int dirZ, int distanceSteps) { if (world == null || world.isRemote) { return false; } @@ -778,7 +793,7 @@ public boolean beginRegionScan(int dirX, int dirY, int dirZ, int distanceSectors } // Re-aiming mid-sweep is allowed and costs only the cell in flight: every cell already // resolved is already written to the crystal, so there is nothing else to lose. - activeScan = RegionScan.directed(origin, dirX, dirY, dirZ, distanceSectors, + activeScan = RegionScan.directed(origin, dirX, dirY, dirZ, distanceSteps, world.getTotalWorldTime(), RegionScan.Tuning.fromConfig()); passive = false; lastScanDiscoveries = 0; @@ -811,12 +826,8 @@ public boolean beginPassiveSweep() { if (origin == null) { return false; } - int radius = Math.max(0, ARConfiguration.getCurrentConfig().telescopePassiveRadiusSectors); - GalacticCoord lo = GalacticCoord.ofSectorLocal(origin.sectorX() - radius, - origin.sectorY() - radius, origin.sectorZ() - radius, 0L, 0L, 0L); - GalacticCoord hi = GalacticCoord.ofSectorLocal(origin.sectorX() + radius, - origin.sectorY() + radius, origin.sectorZ() + radius, 0L, 0L, 0L); - activeScan = RegionScan.box(lo, hi, radius, world.getTotalWorldTime(), + int radius = Math.max(0, ARConfiguration.getCurrentConfig().telescopePassiveRadiusCells); + activeScan = RegionScan.local(origin, radius, world.getTotalWorldTime(), RegionScan.Tuning.fromConfig()); passive = true; lastScanDiscoveries = 0; @@ -835,11 +846,30 @@ public int scanDirectionIndex() { return Math.floorMod(scanDirection, SCAN_DIRECTIONS.length); } - /** How far out the operator has the instrument aimed, in sectors. */ + /** How far out the operator has the instrument aimed, in steps of one star's territory. */ public int getScanDistance() { return scanDistance; } + /** How far the current aim reaches, in light years, or zero before the server has said. */ + public double getAimLightYears() { + return scanDistance * stepLightYears; + } + + /** + * The aim as a length, in brackets — because a bare "3" says nothing about the sky. Empty until + * the server has told this tile what a step is worth, so the client never invents the number. + */ + private String aimInLightYears() { + double ly = getAimLightYears(); + if (ly <= 0d) { + return ""; + } + // The unit travels with the translation: a bracket reading "ly" is English, and this GUI + // already speaks two languages. + return String.format(LibVulpes.proxy.getLocalizedString("msg.observetory.scan.lightyears"), ly); + } + /** Whether the instrument is watching its own neighbourhood rather than a distant patch. */ public boolean isPassive() { return passive; @@ -1054,7 +1084,7 @@ else if (id == PICK_DIRECTION || id == PICK_DISTANCE) { if (id == PICK_DIRECTION) { scanDirection = (scanDirectionIndex() + 1) % SCAN_DIRECTIONS.length; } else { - int reach = Math.max(1, ARConfiguration.getCurrentConfig().telescopeScanRangeSectors); + int reach = RegionScan.Tuning.fromConfig().maxRangeSteps(); scanDistance = Math.max(1, Math.min(reach, scanDistance + nbt.getInteger("d"))); } markDirty(); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java b/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java index 21f07954b..5c1eda1fa 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java @@ -6,14 +6,22 @@ import zmaster587.advancedRocketry.space.GalacticCoord; /** - * A telescope's survey of one region of the galaxy: which box of sectors it covers, how far through + * A telescope's survey of one region of the galaxy: which box of cells it covers, how far through * it the instrument has got, and when the next batch of cells is resolved. * - *

A survey sweeps. It walks its region cell by cell, a bounded number of cells per step, - * writing what each one holds as it goes — an operator points the instrument at a patch of sky once - * and the machine works through it, rather than being re-aimed by hand for every cell. That bound is - * what keeps a procedurally endless universe from being enumerated in a tick; the reach bound keeps - * the patch inside the local cluster.

+ *

A survey sweeps. It walks its region a cell at a time, a bounded number of cells per + * step, writing what each one holds as it goes — an operator points the instrument at a patch of sky + * once and the machine works through it, rather than being re-aimed by hand for every cell. That + * bound is what keeps a procedurally endless universe from being enumerated in a tick; the reach + * bound keeps the patch inside the local cluster.

+ * + *

It samples, it does not enumerate. Between two cells it looks at lies a whole star's + * territory — the survey strides by {@link Tuning#strideCells()}, which is the edge of the cube that + * holds at most one system. Walking cell by cell would spend a whole sweep re-reading one system's + * own neighbourhood, since every cell of a system's territory resolves to that same system; striding + * by the territory means a sweep of N cells looks at N candidate systems. In a star CLUSTER, where + * the lattice is subdivided below that edge, a survey therefore samples the cluster rather than + * emptying it: what is inside one stride and off the sampled cell is left for another look.

* *

Each step is a deadline, never a counter: the tick the next batch lands is stored, so a * survey whose observatory unloads mid-sweep resumes exactly where it stood, owing no replay.

@@ -25,7 +33,8 @@ public final class RegionScan { private static final String KEY_MIN = "min"; private static final String KEY_MAX = "max"; - private static final String KEY_DISTANCE = "dist"; + private static final String KEY_DISTANCE = "distCells"; + private static final String KEY_STRIDE = "stride"; private static final String KEY_START = "start"; private static final String KEY_STEP_DEADLINE = "stepDeadline"; private static final String KEY_CELLS_DONE = "cellsDone"; @@ -34,18 +43,21 @@ public final class RegionScan { private final GalacticCoord min; private final GalacticCoord max; - private final int distanceSectors; + private final long distanceCells; + private final long strideCells; private final long startTick; private final long stepDeadline; private final int cellsDone; private final int cellsPerStep; private final int ticksPerStep; - private RegionScan(GalacticCoord min, GalacticCoord max, int distanceSectors, long startTick, - long stepDeadline, int cellsDone, int cellsPerStep, int ticksPerStep) { + private RegionScan(GalacticCoord min, GalacticCoord max, long distanceCells, long strideCells, + long startTick, long stepDeadline, int cellsDone, int cellsPerStep, + int ticksPerStep) { this.min = min; this.max = max; - this.distanceSectors = distanceSectors; + this.distanceCells = Math.max(0L, distanceCells); + this.strideCells = Math.max(1L, strideCells); this.startTick = startTick; this.stepDeadline = stepDeadline; this.cellsDone = cellsDone; @@ -54,18 +66,20 @@ private RegionScan(GalacticCoord min, GalacticCoord max, int distanceSectors, lo } /** - * Aim a survey from {@code origin} along a direction, {@code distanceSectors} sectors out. + * Aim a survey from {@code origin} along a direction, {@code distanceSteps} star territories out. * *

The direction is taken as a sign per axis, so any vector pointing the same way aims the same - * survey. The distance is clamped into {@code [1, maxRange]} rather than refused: an operator who - * asks for more than the instrument can reach gets the instrument's reach, which is what a - * horizon means.

+ * survey. The distance is counted in STEPS — one step is one star's territory, the same stride + * the sweep walks by — so an aim of 3 means "three stars out", not three cells, which would be a + * fraction of one system. It is clamped into {@code [1, maxRangeSteps]} rather than refused: an + * operator who asks for more than the instrument can reach gets the instrument's reach, which is + * what a horizon means.

* * @throws IllegalArgumentException if there is no origin, or the direction is the zero vector — * a survey with no direction does not name a region. */ public static RegionScan directed(GalacticCoord origin, int dirX, int dirY, int dirZ, - int distanceSectors, long startTick, Tuning tuning) { + int distanceSteps, long startTick, Tuning tuning) { if (origin == null) { throw new IllegalArgumentException("a region survey needs an origin to aim from"); } @@ -79,28 +93,60 @@ public static RegionScan directed(GalacticCoord origin, int dirX, int dirY, int throw new IllegalArgumentException("a survey with no direction does not name a region"); } - int distance = Math.max(1, Math.min(distanceSectors, tuning.maxRangeSectors())); - int half = tuning.effectiveHalfWidthSectors(); + long stride = tuning.strideCells(); + int steps = Math.max(1, Math.min(distanceSteps, tuning.maxRangeSteps())); + long distance = steps * stride; + long half = tuning.effectiveHalfWidthSteps() * stride; long cx = origin.sectorX() + (long) dx * distance; long cy = origin.sectorY() + (long) dy * distance; long cz = origin.sectorZ() + (long) dz * distance; - return box(GalacticCoord.ofSectorLocal(cx - half, cy - half, cz - half, 0L, 0L, 0L), + return new RegionScan( + GalacticCoord.ofSectorLocal(cx - half, cy - half, cz - half, 0L, 0L, 0L), GalacticCoord.ofSectorLocal(cx + half, cy + half, cz + half, 0L, 0L, 0L), - distance, startTick, tuning); + distance, stride, startTick, startTick + stepTicks(distance, tuning), + 0, tuning.cellsPerStep(), stepTicks(distance, tuning)); } /** - * A survey of an explicit box — how the passive local radar states its own neighbourhood, where - * there is no direction to aim and the distance is simply how far the box reaches. + * The passive local radar: a box of {@code radiusCells} cells around {@code origin}, walked cell + * by cell. + * + *

Its stride is ONE CELL and that is deliberate — this is a radar over the observatory's own + * neighbourhood, where the cells really are the interesting granularity (the planet in the next + * cell over is a different destination from its star). The directed survey is the one that looks + * far away, and it is the one that strides by star territories.

*/ - public static RegionScan box(GalacticCoord lo, GalacticCoord hi, int distanceSectors, - long startTick, Tuning tuning) { - int ticksPerStep = Math.max(0, tuning.baseTicks() - + tuning.ticksPerSector() * Math.max(0, distanceSectors)); - return new RegionScan(lo, hi, distanceSectors, startTick, startTick + ticksPerStep, - 0, tuning.cellsPerStep(), ticksPerStep); + public static RegionScan local(GalacticCoord origin, int radiusCells, long startTick, + Tuning tuning) { + if (origin == null) { + throw new IllegalArgumentException("a local radar needs the cell it is standing in"); + } + if (tuning == null) { + throw new IllegalArgumentException("a region survey needs its bounds"); + } + long radius = Math.max(0, radiusCells); + int ticks = stepTicks(radius, tuning); + return new RegionScan( + GalacticCoord.ofSectorLocal(origin.sectorX() - radius, origin.sectorY() - radius, + origin.sectorZ() - radius, 0L, 0L, 0L), + GalacticCoord.ofSectorLocal(origin.sectorX() + radius, origin.sectorY() + radius, + origin.sectorZ() + radius, 0L, 0L, 0L), + radius, 1L, startTick, startTick + ticks, 0, tuning.cellsPerStep(), ticks); + } + + /** + * What one step of a survey aimed {@code distanceCells} away costs, in ticks: a fixed cost for + * holding the instrument on a patch of sky at all, plus a price per light year of distance. + * + *

The distance is converted to light years before it is priced, so what a far look costs stays + * put when the cell edge or the star spacing is retuned.

+ */ + private static int stepTicks(long distanceCells, Tuning tuning) { + double ticks = tuning.baseTicks() + + tuning.ticksPerLightYear() * UniverseScale.lightYearsForCells(distanceCells); + return (int) Math.max(0L, Math.min(Integer.MAX_VALUE, Math.round(ticks))); } /** The inclusive low corner of the surveyed sector box. */ @@ -113,9 +159,19 @@ public GalacticCoord max() { return max; } - /** How far out the survey was aimed, after the range clamp. */ - public int distanceSectors() { - return distanceSectors; + /** How far out the survey was aimed, in cells, after the range clamp. */ + public long distanceCells() { + return distanceCells; + } + + /** The same reach in light years — the form the number is recognisable in. */ + public double distanceLightYears() { + return UniverseScale.lightYearsForCells(distanceCells); + } + + /** How far apart the cells this survey looks at stand. One star's territory, or one cell. */ + public long strideCells() { + return strideCells; } public long startTick() { @@ -140,15 +196,23 @@ public int ticksPerStep() { return ticksPerStep; } - /** How many cells the region holds. Bounded at construction; never unbounded. */ + /** + * How many cells this survey LOOKS at — not how many the region contains. The two differ by the + * stride: a region a hundred territories wide is a hundred looks, not a hundred million cells. + * Bounded at construction; never unbounded. + */ public int totalCells() { - long sx = max.sectorX() - min.sectorX() + 1L; - long sy = max.sectorY() - min.sectorY() + 1L; - long sz = max.sectorZ() - min.sectorZ() + 1L; - long cells = sx * sy * sz; + long cells = countAlong(min.sectorX(), max.sectorX()) + * countAlong(min.sectorY(), max.sectorY()) + * countAlong(min.sectorZ(), max.sectorZ()); return (int) Math.min(Integer.MAX_VALUE, Math.max(0L, cells)); } + /** How many sampled cells one axis of the region holds, at this survey's stride. */ + private long countAlong(long lo, long hi) { + return Math.max(0L, (hi - lo) / strideCells + 1L); + } + /** {@code true} once every cell of the region has been resolved. */ public boolean isComplete() { return cellsDone >= totalCells(); @@ -175,19 +239,19 @@ public long estimatedTicks() { } /** - * The cell at {@code index} in the sweep order: rows along X, then Z, then Y. The order is - * deterministic so a resumed sweep continues where it stopped rather than starting over. + * The cell at {@code index} in the sweep order: rows along X, then Z, then Y, a stride apart. The + * order is deterministic so a resumed sweep continues where it stopped rather than starting over. */ public GalacticCoord cellAt(int index) { - long width = max.sectorX() - min.sectorX() + 1L; - long depth = max.sectorZ() - min.sectorZ() + 1L; + long width = countAlong(min.sectorX(), max.sectorX()); + long depth = countAlong(min.sectorZ(), max.sectorZ()); long perLayer = width * depth; long y = index / perLayer; long rest = index % perLayer; long z = rest / width; long x = rest % width; - return GalacticCoord.ofSectorLocal(min.sectorX() + x, min.sectorY() + y, min.sectorZ() + z, - 0L, 0L, 0L); + return GalacticCoord.ofSectorLocal(min.sectorX() + x * strideCells, + min.sectorY() + y * strideCells, min.sectorZ() + z * strideCells, 0L, 0L, 0L); } /** How many cells the batch due at {@code now} covers — the per-step bound, or what is left. */ @@ -201,13 +265,13 @@ public int cellsDueAt(long now) { /** The survey after a batch of {@code resolved} cells has been written, with its next deadline. */ public RegionScan advanced(long now, int resolved) { int done = Math.min(totalCells(), cellsDone + Math.max(0, resolved)); - return new RegionScan(min, max, distanceSectors, startTick, now + ticksPerStep, done, - cellsPerStep, ticksPerStep); + return new RegionScan(min, max, distanceCells, strideCells, startTick, now + ticksPerStep, + done, cellsPerStep, ticksPerStep); } /** The survey with every cell resolved — the instant path, where time is not the mechanic. */ public RegionScan completed(long now) { - return new RegionScan(min, max, distanceSectors, startTick, now, totalCells(), + return new RegionScan(min, max, distanceCells, strideCells, startTick, now, totalCells(), cellsPerStep, ticksPerStep); } @@ -220,7 +284,8 @@ public void writeToNBT(NBTTagCompound nbt) { max.writeToNBT(hi); nbt.setTag(KEY_MAX, hi); - nbt.setInteger(KEY_DISTANCE, distanceSectors); + nbt.setLong(KEY_DISTANCE, distanceCells); + nbt.setLong(KEY_STRIDE, strideCells); nbt.setLong(KEY_START, startTick); nbt.setLong(KEY_STEP_DEADLINE, stepDeadline); nbt.setInteger(KEY_CELLS_DONE, cellsDone); @@ -236,7 +301,8 @@ public static RegionScan readFromNBT(NBTTagCompound nbt) { return new RegionScan( GalacticCoord.readFromNBT(nbt.getCompoundTag(KEY_MIN)), GalacticCoord.readFromNBT(nbt.getCompoundTag(KEY_MAX)), - nbt.getInteger(KEY_DISTANCE), + nbt.getLong(KEY_DISTANCE), + nbt.getLong(KEY_STRIDE), nbt.getLong(KEY_START), nbt.getLong(KEY_STEP_DEADLINE), nbt.getInteger(KEY_CELLS_DONE), @@ -253,48 +319,72 @@ public String toString() { /** * What bounds a survey and what it costs in time. Every number here is balance, not contract: the * reach, the size of the patch, how many cells one step resolves and how long a step takes. + * + *

The reach is stated as a LENGTH — light years, the unit a telescope's horizon is quoted in — + * and converted here against the stride. Stating it as a count of anything would make the + * instrument's horizon move whenever the star spacing or the cell edge was retuned, which is how + * a reach came to mean a fifth of the way to Mercury.

*/ public static final class Tuning { - private final int maxRangeSectors; - private final int halfWidthSectors; - private final int maxSectors; + private final double maxRangeLightYears; + private final int halfWidthSteps; + private final int maxCells; private final int baseTicks; - private final int ticksPerSector; + private final double ticksPerLightYear; private final int cellsPerStep; + private final long strideCells; - public Tuning(int maxRangeSectors, int halfWidthSectors, int maxSectors, - int baseTicks, int ticksPerSector, int cellsPerStep) { - this.maxRangeSectors = Math.max(1, maxRangeSectors); - this.halfWidthSectors = Math.max(0, halfWidthSectors); - this.maxSectors = Math.max(1, maxSectors); + public Tuning(double maxRangeLightYears, int halfWidthSteps, int maxCells, int baseTicks, + double ticksPerLightYear, int cellsPerStep, long strideCells) { + this.maxRangeLightYears = Math.max(0d, maxRangeLightYears); + this.halfWidthSteps = Math.max(0, halfWidthSteps); + this.maxCells = Math.max(1, maxCells); this.baseTicks = Math.max(0, baseTicks); - this.ticksPerSector = Math.max(0, ticksPerSector); + this.ticksPerLightYear = Math.max(0d, ticksPerLightYear); this.cellsPerStep = Math.max(1, cellsPerStep); + this.strideCells = Math.max(1L, strideCells); } - /** The tuning the running game is configured with. */ + /** + * The tuning the running game is configured with — including the stride, which is the active + * generator's own star spacing and never a number of its own: a survey that strode by + * anything else would either re-read one system or step over whole ones. + */ public static Tuning fromConfig() { ARConfiguration config = ARConfiguration.getCurrentConfig(); return new Tuning( - config.telescopeScanRangeSectors, - config.telescopeScanHalfWidthSectors, - config.telescopeScanMaxSectors, + config.telescopeScanRangeLightYears, + config.telescopeScanHalfWidthSteps, + config.telescopeScanMaxCells, config.telescopeScanBaseTicks, - config.telescopeScanTicksPerSector, - config.telescopeScanCellsPerStep); + config.telescopeScanTicksPerLightYear, + config.telescopeScanCellsPerStep, + UniverseRegistry.getGenerator().minSpacingCells()); + } + + /** The instrument's horizon, as a length. */ + public double maxRangeLightYears() { + return maxRangeLightYears; + } + + /** How far apart the cells a directed survey looks at stand — one star's territory. */ + public long strideCells() { + return strideCells; } - public int maxRangeSectors() { - return maxRangeSectors; + /** The horizon as a number of steps, which is what an operator aims in. At least one. */ + public int maxRangeSteps() { + long steps = UniverseScale.cellsForLightYears(maxRangeLightYears) / strideCells; + return (int) Math.max(1L, Math.min(Integer.MAX_VALUE, steps)); } public int baseTicks() { return baseTicks; } - public int ticksPerSector() { - return ticksPerSector; + public double ticksPerLightYear() { + return ticksPerLightYear; } public int cellsPerStep() { @@ -302,13 +392,13 @@ public int cellsPerStep() { } /** - * The half-width a survey actually gets: the configured one, narrowed until the region fits - * inside the sector ceiling. The ceiling wins over the width — a sweep may be long, but it - * may not be unbounded. + * The half-width a survey actually gets, in steps: the configured one, narrowed until the + * number of cells it would look at fits inside the ceiling. The ceiling wins over the width — + * a sweep may be long, but it may not be unbounded. */ - public int effectiveHalfWidthSectors() { - int half = halfWidthSectors; - while (half > 0 && volumeOf(half) > maxSectors) { + public int effectiveHalfWidthSteps() { + int half = halfWidthSteps; + while (half > 0 && volumeOf(half) > maxCells) { half--; } return half; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java index f930f9c10..2455a9e01 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java @@ -1,6 +1,6 @@ package zmaster587.advancedRocketry.universe; -import java.util.Map; +import java.util.Optional; import java.util.function.IntFunction; import net.minecraft.item.ItemStack; @@ -72,31 +72,39 @@ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int f } /** - * Resolve ONE cell: every body of the system standing there, or the bare coordinate when the - * system has no content the registry can name. + * Resolve ONE cell: every body of the system that OWNS it, or the bare coordinate when that + * system has no content the registry can name. Void space yields nothing, which is the point of + * asking at all — an empty sky must not manufacture an address. + * + *

The question is which system owns this cell, never "is a star seated exactly here". + * A system is a neighbourhood: its star holds the anchor cell and every planet holds one of its + * own, so a cell that is a system's planet — or simply the space between its bodies — is a cell + * that resolves to that system. Asking whether the cell IS the seat means a survey discovers a + * system only by landing on its star's own address, which for a lattice a few thousand cells wide + * is a thing that never happens. Resolving through the owner is also what lets an observatory + * standing on a planet report the system it is standing in.

*/ public static int resolveCell(UniverseRegistry registry, GalacticCoord cell, CrystalMemory memory, long observedTick, IntFunction nameOf) { if (registry == null || cell == null || memory == null) { return 0; } - Map here = registry.systemsInRegion(cell, cell); - if (here.isEmpty()) { + Optional anchor = registry.anchorForCell(cell); + if (!anchor.isPresent()) { return 0; } int written = 0; boolean namedSomething = false; - for (SystemBody body : registry.systemBodiesAt(cell)) { + for (SystemBody body : registry.systemBodiesAt(anchor.get())) { namedSomething = true; if (memory.record(entryFor(body, observedTick, nameOf))) { written++; } } if (!namedSomething) { - for (Map.Entry system : here.entrySet()) { - if (memory.record(entryForSystem(system.getKey(), system.getValue(), observedTick))) { - written++; - } + StarSystem system = registry.systemForCoord(anchor.get()).orElse(null); + if (memory.record(entryForSystem(anchor.get(), system, observedTick))) { + written++; } } return written; diff --git a/src/main/resources/assets/advancedrocketry/lang/en_US.lang b/src/main/resources/assets/advancedrocketry/lang/en_US.lang index 6d8c298f2..f0b7052d8 100644 --- a/src/main/resources/assets/advancedrocketry/lang/en_US.lang +++ b/src/main/resources/assets/advancedrocketry/lang/en_US.lang @@ -401,8 +401,9 @@ msg.observetory.scan.button=Scan! msg.observetory.scan.crystal=Memory crystal msg.observetory.scan.direction=Aimed at: msg.observetory.scan.direction.tooltip=Turn the instrument to the next patch of sky -msg.observetory.scan.distance=Distance (sectors): -msg.observetory.scan.distance.tooltip=How far out to look. Farther is a longer observation, and the instrument has a horizon. +msg.observetory.scan.distance=Distance (stars): +msg.observetory.scan.distance.tooltip=How far out to look, counted in neighbouring stars. Farther is a longer observation, and the instrument has a horizon. +msg.observetory.scan.lightyears= (%.1f ly) msg.observetory.scan.region=Observe msg.observetory.scan.region.tooltip=Look at the chosen region and write every system it resolves onto the crystal msg.observetory.scan.looking=Surveyed cells: diff --git a/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang b/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang index b22ddea47..d62deef50 100644 --- a/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang +++ b/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang @@ -252,8 +252,9 @@ msg.observetory.scan.button=Сканировать! msg.observetory.scan.crystal=Кристалл памяти msg.observetory.scan.direction=Наведение: msg.observetory.scan.direction.tooltip=Повернуть инструмент к следующему участку неба -msg.observetory.scan.distance=Дальность (секторов): -msg.observetory.scan.distance.tooltip=Как далеко смотреть. Дальше — дольше наблюдение, и у инструмента есть горизонт. +msg.observetory.scan.distance=Дальность (звёзд): +msg.observetory.scan.distance.tooltip=Как далеко смотреть, в соседних звёздах. Дальше — дольше наблюдение, и у инструмента есть горизонт. +msg.observetory.scan.lightyears= (%.1f св. лет) msg.observetory.scan.region=Наблюдать msg.observetory.scan.region.tooltip=Осмотреть выбранную область и записать в кристалл все системы, которые она разрешит msg.observetory.scan.looking=Осмотрено ячеек: diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/MachineGuiClientGroupE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/MachineGuiClientGroupE2ETest.java index d928bb677..4b9a113dc 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/MachineGuiClientGroupE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/MachineGuiClientGroupE2ETest.java @@ -100,6 +100,8 @@ public class MachineGuiClientGroupE2ETest extends AbstractSharedClientE2ETest { // Observatory region-scan probe fields. private static final Pattern TELESCOPE_ORIGIN = Pattern.compile("\"origin\":\"([^\"]*)\""); private static final Pattern TELESCOPE_AIM_DISTANCE = Pattern.compile("\"aimDistance\":(\\d+)"); + /** What one step of the aim is worth in cells — the aim is counted in star territories. */ + private static final Pattern TELESCOPE_STEP_CELLS = Pattern.compile("\"stepCells\":(\\d+)"); private static final Pattern TELESCOPE_ADDRESSES = Pattern.compile("\"addresses\":(-?\\d+)"); // Railgun probe fields. @@ -504,9 +506,9 @@ public void theOperatorAimsTheTelescopeAndObservesWithNothingButClicks() throws // is what this drives. exec("artest config set planetsMustBeDiscovered false"); exec("artest config set telescopeScanBaseTicks 0"); - exec("artest config set telescopeScanTicksPerSector 1"); - exec("artest config set telescopeScanHalfWidthSectors 1"); - exec("artest config set telescopeScanRangeSectors 24"); + exec("artest config set telescopeScanTicksPerLightYear 1"); + exec("artest config set telescopeScanHalfWidthSteps 1"); + exec("artest config set telescopeScanRangeLightYears 100"); String crystal = exec("artest telescope crystal " + where); scenario().requireArranged("could not put a crystal in the observatory: " + crystal, crystal.contains("\"ok\":true")); @@ -535,9 +537,13 @@ public void theOperatorAimsTheTelescopeAndObservesWithNothingButClicks() throws assertTrue("clicking the distance button twice must move the aim out from 1: " + aimed, aimDistance > 1); - // Put a system exactly where the operator has it pointed — the default aim is +X, and the - // distance is whatever his clicks produced. - String system = exec("artest telescope system " + (Long.parseLong(home[0]) + aimDistance) + // Put a system where the operator has it pointed — the default aim is +X, and the distance is + // whatever his clicks produced. The aim is counted in STEPS of one star's territory, so the + // cell it lands on is that many strides out; the seat is offset inside the territory, since + // what a look must find is the system that OWNS the cell and not a star standing on it. + long stepCells = readInt(aimed, TELESCOPE_STEP_CELLS); + String system = exec("artest telescope system " + + (Long.parseLong(home[0]) + aimDistance * stepCells + 13L) + " " + home[1] + " " + home[2]); scenario().requireArranged("could not place a system to be found: " + system, system.contains("\"ok\":true")); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/TelescopeRegionScanE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/TelescopeRegionScanE2ETest.java index fe24e4d58..48e914e2a 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/TelescopeRegionScanE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/TelescopeRegionScanE2ETest.java @@ -16,7 +16,7 @@ * *

Every number here is the SERVER's answer; the probes state their own side.

* - *

Position-isolated at x=4300-4460 (clear of the observatory-multiblock fixtures at x=4000-4060).

+ *

Position-isolated at x=4300-4660 (clear of the observatory-multiblock fixtures at x=4000-4060).

*/ public class TelescopeRegionScanE2ETest extends AbstractSharedServerTest { @@ -36,15 +36,26 @@ private static String join(java.util.List response) { * the default game, where what the instrument reaches is resolved outright; on is the research * mode, where the sweep is paced and the time curve is the mechanic. */ - private void surveySetup(boolean research, int cellsPerStep, int ticksPerSector) throws Exception { + private void surveySetup(boolean research, int cellsPerStep, double ticksPerLightYear) + throws Exception { exec("artest config set planetsMustBeDiscovered " + research); exec("artest config set telescopeScanBaseTicks 0"); - exec("artest config set telescopeScanTicksPerSector " + ticksPerSector); - exec("artest config set telescopeScanRangeSectors 24"); - exec("artest config set telescopeScanHalfWidthSectors 1"); - exec("artest config set telescopeScanMaxSectors 1000"); + exec("artest config set telescopeScanTicksPerLightYear " + ticksPerLightYear); + exec("artest config set telescopeScanRangeLightYears 100"); + exec("artest config set telescopeScanHalfWidthSteps 1"); + exec("artest config set telescopeScanMaxCells 1000"); exec("artest config set telescopeScanCellsPerStep " + cellsPerStep); - exec("artest config set telescopePassiveRadiusSectors 1"); + exec("artest config set telescopePassiveRadiusCells 1"); + } + + /** How far apart, in cells, the looks of a directed survey stand in THIS server's universe. */ + private long stride(int x) throws Exception { + String started = exec("artest telescope scan " + where(x) + " 1 0 0 1"); + assertTrue("could not aim the instrument to read its stride: " + started, + started.contains("\"ok\":true")); + long stride = field(started, "stride"); + exec("artest telescope abort " + where(x)); + return stride; } /** The value of a numeric JSON field in a probe reply. */ @@ -60,6 +71,19 @@ private static long field(String json, String name) { return Long.parseLong(json.substring(from, to)); } + /** The value of a decimal JSON field in a probe reply — a length, not a count. */ + private static double decimal(String json, String name) { + String key = "\"" + name + "\":"; + int at = json.indexOf(key); + assertTrue("probe reply has no field " + name + ": " + json, at >= 0); + int from = at + key.length(); + int to = from; + while (to < json.length() && "-+.eE0123456789".indexOf(json.charAt(to)) >= 0) { + to++; + } + return Double.parseDouble(json.substring(from, to)); + } + /** The value of a string JSON field in a probe reply. */ private static String text(String json, String name) { String key = "\"" + name + "\":\""; @@ -92,6 +116,18 @@ private void systemAt(long sx, String sy, String sz) throws Exception { assertTrue("could not place a system to be found: " + system, system.contains("\"ok\":true")); } + /** + * Seat a system {@code steps} territories out along +X, deliberately OFF the cell the survey + * looks at. + * + *

The offset is the point of the fixture: a star is one cell of a territory millions of cells + * wide, so a survey that could see a system only by landing on its star's own address finds + * nothing. What must be found is the system that OWNS the cell that was looked at.

+ */ + private void systemNearTheLookAt(int x, String[] home, int steps) throws Exception { + systemAt(Long.parseLong(home[0]) + steps * stride(x) + 13L, home[1], home[2]); + } + /** Poll the machine until its survey is finished. Bounded: 40 × 250 ms = 10 s. */ private String awaitSurveyComplete(int x) throws Exception { String info = ""; @@ -110,7 +146,7 @@ public void withoutResearchWhatTheInstrumentReachesIsResolvedOutright() throws E final int x = 4300; surveySetup(false, 2, 40); String[] home = observatoryWithCrystal(x); - systemAt(Long.parseLong(home[0]) + 4, home[1], home[2]); + systemNearTheLookAt(x, home, 4); String started = exec("artest telescope scan " + where(x) + " 1 0 0 4"); assertTrue("the survey did not start: " + started, started.contains("\"ok\":true")); @@ -128,7 +164,8 @@ public void withResearchTheSurveySweepsCellByCell() throws Exception { // One cell a step — the claim under test — and a step short enough that 27 of them fit in // the poll budget: at 20 ticks per sector of distance a single cell took 4 s, so the whole // region wanted 108 s against a 10 s budget and the sweep was blamed for the arithmetic. - surveySetup(true, 1, 1); + // Priced per LIGHT YEAR now, and four steps out is ~17 of them, so the rate is a fraction. + surveySetup(true, 1, 0.2); String[] home = observatoryWithCrystal(x); String started = exec("artest telescope scan " + where(x) + " 1 0 0 4"); @@ -159,7 +196,7 @@ public void stoppingASurveyIsFreeAndKeepsWhatWasAlreadyLearned() throws Exceptio final int x = 4380; surveySetup(true, 1, 60); String[] home = observatoryWithCrystal(x); - systemAt(Long.parseLong(home[0]) + 3, home[1], home[2]); + systemNearTheLookAt(x, home, 3); exec("artest telescope scan " + where(x) + " 1 0 0 3"); String before = exec("artest telescope info " + where(x)); @@ -212,6 +249,15 @@ public void theLocalRadarSurveysTheObservatorysOwnNeighbourhood() throws Excepti long hi = Long.parseLong(maxKey.split("_")[0]); assertTrue("the radar must look around home (" + homeX + "), not at " + minKey + ".." + maxKey, lo <= homeX && homeX <= hi); + assertEquals("and it must walk CELLS: close to home the next cell is a different destination", + 1L, field(passive, "stride")); + + // An observatory stands on a PLANET, never on its own star. Under the gate this test was + // written against, the cell it is standing in reported empty and the machine could not name + // the system it was sitting in. + String done = awaitSurveyComplete(x); + assertTrue("the radar must resolve the system the observatory is standing in: " + done, + field(done, "addresses") >= 1); } @Test @@ -271,7 +317,7 @@ public void whatTheTelescopeWroteIsWhatAShipCanBeAimedBy() throws Exception { final int x = 4620; surveySetup(false, 4, 1); String[] home = observatoryWithCrystal(x); - systemAt(Long.parseLong(home[0]) + 3, home[1], home[2]); + systemNearTheLookAt(x, home, 3); exec("artest telescope scan " + where(x) + " 1 0 0 3"); String surveyed = awaitSurveyComplete(x); @@ -292,6 +338,30 @@ public void whatTheTelescopeWroteIsWhatAShipCanBeAimedBy() throws Exception { field(status, "ship") >= 1); } + @Test + public void theHorizonIsALengthAnInstrumentCouldActuallyHave() throws Exception { + final int x = 4660; + // The half of the defect that no amount of resolving would have fixed: the reach was stated + // in cells, so 24 of them was 0.16 AU — a fifth of the way to Mercury — and every aim inside + // the horizon stayed inside the solar system. A horizon is a LENGTH. + surveySetup(false, 4, 1); + observatoryWithCrystal(x); + + String idle = exec("artest telescope info " + where(x)); + double reachLy = decimal(idle, "reachLy"); + assertTrue("a telescope's horizon must reach other stars, in light years: " + reachLy, + reachLy >= 4d); + assertTrue("and must buy more than one star's territory: " + field(idle, "reachSteps"), + field(idle, "reachSteps") >= 2); + + String aimed = exec("artest telescope scan " + where(x) + " 1 0 0 " + field(idle, "reachSteps")); + assertTrue("the survey did not start: " + aimed, aimed.contains("\"ok\":true")); + assertTrue("an aim at the horizon must land an interstellar distance away: " + + decimal(aimed, "distanceLy") + " ly", + decimal(aimed, "distanceLy") >= 4d); + exec("artest telescope abort " + where(x)); + } + @Test public void aFartherRegionIsALongerSurveyOnTheRealClock() throws Exception { final int x = 4500; diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java index 4da4cbd11..40a35b646 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java @@ -10,6 +10,7 @@ import zmaster587.advancedRocketry.navigation.CrystalMemory; import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.universe.EmptyGalaxyGenerator; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.InfoTier; import zmaster587.advancedRocketry.universe.RegionScan; import zmaster587.advancedRocketry.universe.SystemBody; @@ -30,19 +31,27 @@ * writes onto a crystal. Pure-JUnit — no MC bootstrap; the registry's generator and star lookup are * the injectable seams. * - *

These pin player-facing promises — a far survey costs more than a near one, the horizon is the - * configured reach, one step never enumerates the sky, an unknown system is discoverable, and what a - * telescope writes is a BODY at the coarsest grade, dated — plus the save contract that a sweep - * outlives the chunk it started in and resumes where it stood. They do not pin the time formula, the - * sweep order or the storage shape.

+ *

These pin player-facing promises — a far survey costs more than a near one, the horizon is a + * LENGTH a telescope could have, a look finds the system that OWNS the cell rather than only a star + * seated on it, empty sky stays empty, one step never enumerates the sky, an unknown system is + * discoverable, and what a telescope writes is a BODY at the coarsest grade, dated — plus the save + * contract that a sweep outlives the chunk it started in and resumes where it stood. They do not pin + * the time formula, the sweep order or the storage shape.

*/ public class TelescopeRegionScanTest { private static final GalacticCoord HOME = GalacticCoord.ofSectorLocal(0, 0, 0, 0, 0, 0); - /** Reach 10 sectors, a 3×3×3 region, room for it, 100 ticks a step plus 50 per sector, 2 cells a step. */ + /** + * One survey STEP: the edge of the cube that holds at most one system, which is what the sweep + * strides by and what an operator's aim is counted in. Taken from the generator rather than + * invented — the registry attributes a member cell to its system by the same number. + */ + private static final long STEP = GalaxyGenConfig.DEFAULT_MIN_SPACING; + + /** Reach 50 light years, a 3×3×3 patch of territories, room for it, 100 ticks a step + 50 a ly. */ private static RegionScan.Tuning tuning() { - return new RegionScan.Tuning(10, 1, 512, 100, 50, 2); + return new RegionScan.Tuning(50d, 1, 512, 100, 50d, 2, STEP); } private static StellarBody star(int id) { @@ -56,6 +65,11 @@ private static GalacticCoord cell(long x, long y, long z) { return GalacticCoord.ofSectorLocal(x, y, z, 0L, 0L, 0L); } + /** The cell {@code steps} territories out along +X — where an aim of {@code steps} lands. */ + private static GalacticCoord stepsOut(long steps) { + return cell(steps * STEP, 0, 0); + } + @After public void resetSeams() { UniverseRegistry.setGenerator(null); @@ -75,15 +89,38 @@ public void aFartherRegionIsALongerSurvey() { far.estimatedTicks() > near.estimatedTicks()); } + @Test + public void theReachIsALengthAndTheAimIsCountedInStars() { + // The defect this replaced: a reach stated in cells read as 0.16 AU — a fifth of the way to + // Mercury — and no aim inside it could ever leave the solar system. A horizon is a LENGTH, + // and what it buys is a number of star territories, so both must be recognisable. + RegionScan.Tuning tuning = tuning(); + + assertTrue("a telescope's horizon must be quoted in light years: " + tuning.maxRangeLightYears(), + tuning.maxRangeLightYears() >= 1d); + assertTrue("and must reach at least the nearest few stars, or nothing is discoverable: " + + tuning.maxRangeSteps() + " steps", + tuning.maxRangeSteps() >= 3); + + RegionScan aimed = RegionScan.directed(HOME, 1, 0, 0, 3, 0L, tuning); + assertEquals("an aim of three stars must land three territories out, not three cells", + stepsOut(3).cellKey(), + cell(aimed.distanceCells(), 0, 0).cellKey()); + assertTrue("and that distance, read as a length, must be interstellar: " + + aimed.distanceLightYears() + " ly", + aimed.distanceLightYears() >= 3d); + } + @Test public void theHorizonIsTheConfiguredReach() { // "You cannot see beyond your own cluster": an aim past the reach is answered at the reach, // and costs exactly what looking at the reach costs — not more. - RegionScan reached = RegionScan.directed(HOME, 0, 0, 1, 10, 0L, tuning()); + int horizon = tuning().maxRangeSteps(); + RegionScan reached = RegionScan.directed(HOME, 0, 0, 1, horizon, 0L, tuning()); RegionScan overreached = RegionScan.directed(HOME, 0, 0, 1, 9999, 0L, tuning()); assertEquals("an aim past the horizon must be answered at the horizon", - reached.distanceSectors(), overreached.distanceSectors()); + reached.distanceCells(), overreached.distanceCells()); assertEquals("and must cost what the horizon costs", reached.estimatedTicks(), overreached.estimatedTicks()); assertEquals("the region itself must be the one at the horizon", @@ -94,7 +131,7 @@ public void theHorizonIsTheConfiguredReach() { public void oneStepNeverResolvesMoreThanItsCellBudget() { // The structural guard against reading an endless procedural universe off one instrument: // a survey may cover a large region, but never in one step. - RegionScan.Tuning wide = new RegionScan.Tuning(10, 2, 1000, 100, 50, 3); + RegionScan.Tuning wide = new RegionScan.Tuning(50d, 2, 1000, 100, 50d, 3, STEP); RegionScan scan = RegionScan.directed(HOME, 1, 1, 0, 3, 0L, wide); assertTrue("the fixture must be a region worth sweeping", scan.totalCells() > 3); @@ -105,7 +142,7 @@ public void oneStepNeverResolvesMoreThanItsCellBudget() { @Test public void aRegionNeverExceedsItsCeiling() { // Ask for a 9×9×9 region with room for 27 cells and the ceiling wins. - RegionScan.Tuning greedy = new RegionScan.Tuning(10, 4, 27, 100, 50, 2); + RegionScan.Tuning greedy = new RegionScan.Tuning(50d, 4, 27, 100, 50d, 2, STEP); RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, 3, 0L, greedy); assertTrue("a survey may never cover more than its ceiling: " + scan.totalCells(), @@ -152,6 +189,39 @@ public void everyCellOfTheRegionIsVisitedExactlyOnce() { assertEquals("and must cover the whole region", scan.totalCells(), seen.size()); } + @Test + public void aSweepStridesByOneStarsTerritoryRatherThanByCells() { + // What makes a sweep worth its time: every look is a different candidate system. Walking + // cell by cell would spend a whole survey inside one system's own neighbourhood, since + // every cell of that neighbourhood answers with the same system. + RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, 4, 0L, tuning()); + + assertEquals("a directed survey strides by one star's territory", STEP, scan.strideCells()); + assertTrue("the fixture must span more than one look along X", scan.totalCells() > 1); + + long first = scan.cellAt(0).sectorX(); + long second = scan.cellAt(1).sectorX(); + assertEquals("two consecutive looks must be a whole territory apart, not a cell", + STEP, Math.abs(second - first)); + } + + @Test + public void theLocalRadarWalksCellsNotTerritories() { + // The other half of the same decision: close to home the cells ARE the granularity — the + // planet in the next cell is a different destination from its star — so the radar keeps a + // cell stride while the directed survey strides by stars. + RegionScan radar = RegionScan.local(HOME, 1, 0L, tuning()); + + assertEquals("the local radar walks cell by cell", 1L, radar.strideCells()); + assertEquals("a radius of one cell is a 3x3x3 neighbourhood", 27, radar.totalCells()); + + boolean looksAtHome = false; + for (int i = 0; i < radar.totalCells(); i++) { + looksAtHome |= radar.cellAt(i).cellKey().equals(HOME.cellKey()); + } + assertTrue("and it must look at the cell the instrument is standing in", looksAtHome); + } + @Test public void aSurveyWithNoDirectionIsRefused() { try { @@ -189,25 +259,36 @@ public void nothingIsStoredForAnObservatoryThatIsNotLooking() { // ── what a survey discovers ─────────────────────────────────────────────── - /** A registry holding two systems with bodies, plus one well outside the surveyed region. */ + /** + * A registry holding two systems inside the surveyed patch and one well outside it. + * + *

Not one of the three stars is seated on a cell the sweep looks at, and that is the + * fixture's whole point. A star is one cell of a territory millions of cells wide, so a survey + * that could only see a system by landing on its star's own address would find nothing here — + * which is exactly what the instrument used to do. Each seat is offset from the look that must + * find it, by a distance that is inside its own neighbourhood and nowhere near the next.

+ */ private UniverseRegistry threeSystems() { UniverseRegistry.setGenerator(new EmptyGalaxyGenerator()); UniverseRegistry.setStarLookup(TelescopeRegionScanTest::star); UniverseRegistry registry = new UniverseRegistry(); - registry.place(cell(4, 0, 0), 4); - registry.addPoi(SystemBody.fixedAt(cell(4, 0, 0), SystemBodyKind.STAR, Constants.INVALID_PLANET, 4)); - registry.addPoi(SystemBody.fixedAt(cell(4, 0, 0), SystemBodyKind.PLANET, 401, 4)); - - registry.place(cell(5, 1, 0), 5); - registry.addPoi(SystemBody.fixedAt(cell(5, 1, 0), SystemBodyKind.PLANET, 501, 5)); - - registry.place(cell(9, 0, 0), 9); - registry.addPoi(SystemBody.fixedAt(cell(9, 0, 0), SystemBodyKind.PLANET, 901, 9)); + GalacticCoord inner = cell(4 * STEP - 20, 0, 0); // found by the look at 4 steps out + registry.place(inner, 4); + registry.addPoi(SystemBody.fixedAt(inner, SystemBodyKind.STAR, Constants.INVALID_PLANET, 4)); + registry.addPoi(SystemBody.fixedAt(inner, SystemBodyKind.PLANET, 401, 4)); + + GalacticCoord edge = cell(5 * STEP + 7, STEP - 3, 0); // found by the look at the corner + registry.place(edge, 5); + registry.addPoi(SystemBody.fixedAt(edge, SystemBodyKind.PLANET, 501, 5)); + + GalacticCoord beyond = cell(9 * STEP, 0, 0); // far outside the patch + registry.place(beyond, 9); + registry.addPoi(SystemBody.fixedAt(beyond, SystemBodyKind.PLANET, 901, 9)); return registry; } - /** The survey the fixture is built around: 4 sectors out along +X, one sector wide. */ + /** The survey the fixture is built around: 4 territories out along +X, one territory wide. */ private RegionScan boxAroundFourthSector() { return RegionScan.directed(HOME, 1, 0, 0, 4, 0L, tuning()); } @@ -245,6 +326,54 @@ public void aSurveyWritesTheBODIESItResolved() { assertEquals("named the way every other screen names it", "Body-401", planet.name()); } + @Test + public void aSystemIsFoundFromAnyCellItOWNS_notOnlyFromItsStarsSeat() { + // THE defect. A system is a neighbourhood: its star holds one cell of it and its planets hold + // others. Asking "is a star seated exactly here" makes discovery a lottery whose odds are one + // cell in a territory millions wide — so a survey found nothing and reported an empty sky. + // Asking "which system owns this cell" is the same question a telescope asks of the light. + UniverseRegistry registry = threeSystems(); + CrystalMemory crystal = new CrystalMemory(); + + GalacticCoord look = stepsOut(4); + assertFalse("the fixture is worthless unless the look is NOT the star's own seat", + registry.starIdForCoord(look).isPresent()); + + TelescopeScan.resolveCell(registry, look, crystal, 7_000L, dimId -> "Body-" + dimId); + + assertNotNull("a survey must discover the system that OWNS the cell it looked at", + crystal.forBody(401)); + } + + @Test + public void aLookIntoTheVoidDiscoversNothing() { + // The gate exists so that empty sky does not manufacture addresses — and the fix must not + // trade one failure for its opposite by attributing every cell to some system. + UniverseRegistry registry = threeSystems(); + CrystalMemory crystal = new CrystalMemory(); + + int written = TelescopeScan.resolveCell(registry, cell(400 * STEP, 0, 0), crystal, 7_000L, + dimId -> "Body-" + dimId); + + assertEquals("interstellar void must yield no addresses at all", 0, written); + assertEquals("and must write nothing onto the crystal", 0, crystal.size()); + } + + @Test + public void anInstrumentInsideASystemResolvesTheSystemItStandsIn() { + // An observatory does not stand on its own star: it stands on a planet, in one of its + // system's member cells. Under the old gate that cell reported empty, so the machine could + // not name the system it was sitting in — which is also what the local radar is for. + UniverseRegistry registry = threeSystems(); + CrystalMemory crystal = new CrystalMemory(); + GalacticCoord standingOn = cell(4 * STEP - 20 + 5_000, 3_000, 0); // a member cell, not the seat + + TelescopeScan.resolveCell(registry, standingOn, crystal, 7_000L, dimId -> "Body-" + dimId); + + assertNotNull("an instrument inside a system must be able to name that system", + crystal.forBody(401)); + } + @Test public void aSystemTheCrystalNeverHeardOfIsStillDiscovered() { // The discriminator against the tempting wrong shape — reporting only what is already known. @@ -252,7 +381,7 @@ public void aSystemTheCrystalNeverHeardOfIsStillDiscovered() { // new: a knowledge gate anywhere on this path leaves it missing and this test red. UniverseRegistry registry = threeSystems(); CrystalMemory crystal = new CrystalMemory(); - crystal.record(new CrystalEntry(cell(4, 0, 0), "Body-401", SystemBodyKind.PLANET, + crystal.record(new CrystalEntry(stepsOut(4), "Body-401", SystemBodyKind.PLANET, InfoTier.TELESCOPE, 1_000L, 401)); assertNull("the fixture must start ignorant of the body under test", crystal.forBody(501)); @@ -288,7 +417,7 @@ public void aSweepWritesOnlyTheCellsItHasReached() { int firstCellWithContent = -1; for (int i = 0; i < scan.totalCells(); i++) { - if (scan.cellAt(i).cellKey().equals(cell(4, 0, 0).cellKey())) { + if (scan.cellAt(i).cellKey().equals(stepsOut(4).cellKey())) { firstCellWithContent = i; break; } From 8bc78bcdab4a59cfaa6e871992a85fff70d2f6bd Mon Sep 17 00:00:00 2001 From: StannisMod Date: Sat, 15 Aug 2026 18:20:54 +0300 Subject: [PATCH 20/42] feat: a nebula becomes something you can see - ask the generator which clouds are within reach of a cell - send each as a direction and an apparent size, never a position - draw them behind the stars, and the dark ones in front - report what is seated beside what is drawn --- .../client/render/planet/BoundarySky.java | 155 +++++++++++- .../command/test/TestProbeCommand.java | 92 ++++++++ .../network/PacketSystemBodiesSync.java | 130 +++++++++- .../space/SkyNebulaeProducer.java | 223 ++++++++++++++++++ .../space/SystemBodiesProducer.java | 24 +- .../universe/ClusteredGalaxyGenerator.java | 27 +++ .../universe/IGalaxyGenerator.java | 12 + .../BoundarySkyRendersInSlotCellE2ETest.java | 89 +++++++ .../test/server/NebulaSkyFeedE2ETest.java | 102 ++++++++ .../test/unit/SkyNebulaeProducerTest.java | 169 +++++++++++++ 10 files changed, 1011 insertions(+), 12 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/space/SkyNebulaeProducer.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/server/NebulaSkyFeedE2ETest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/BoundarySky.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/BoundarySky.java index 1b9fcd270..a30af478c 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/BoundarySky.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/BoundarySky.java @@ -18,6 +18,7 @@ import zmaster587.advancedRocketry.dimension.DimensionProperties; import zmaster587.advancedRocketry.network.PacketSystemBodiesSync; import zmaster587.advancedRocketry.space.HyperspaceWorld; +import zmaster587.advancedRocketry.universe.Nebula; import zmaster587.advancedRocketry.universe.SystemBodyKind; import java.util.List; @@ -64,6 +65,13 @@ public class BoundarySky extends IRenderHandler { private static final float STAR_ALPHA = 0.9F; + /** Sky-frame radius the nebulae are emitted on. Outside the starfield: a cloud is the backdrop. */ + private static final float NEBULA_SKY_RADIUS = 105.0F; + /** Points around one cloud's rim. A cloud is soft, so it needs far fewer than a hard circle. */ + private static final int NEBULA_SEGMENTS = 24; + /** How bright the densest cloud may draw at its core. Haze, never a light source. */ + private static final float NEBULA_MAX_ALPHA = 0.45F; + /** * How many body labels the last frame actually drew. A counter rather than a flag: the contract * is that the toggle removes the label ENTIRELY, and "zero drawn while bodies were fed" is the @@ -82,6 +90,13 @@ public class BoundarySky extends IRenderHandler { */ public static volatile int boundariesDrawnLastFrame; + /** + * How many nebulae the last frame drew. Same shape and same reason as the two counters above: a + * cloud is haze with no edge, so "is one on the screen" is a question pixels answer badly and the + * renderer answers exactly. Read it beside {@link #skyFramesDrawn}, never alone. + */ + public static volatile int nebulaeDrawnLastFrame; + /** * Frames on which this sky renderer ran AT ALL, counted before any branch inside it. * @@ -124,9 +139,10 @@ public void render(float partialTicks, WorldClient world, Minecraft mc) { GlStateManager.disableTexture2D(); - // Stars first: the billboards are meant to sit in front of them. - GlStateManager.color(1.0F, 1.0F, 1.0F, STAR_ALPHA); - GL11.glCallList(this.glStarList); + // The backdrop: the clouds and the starfield, in the one order that is right for both. The + // billboards below are meant to sit in front of all of it. + nebulaeDrawnLastFrame = drawBackdrop( + PacketSystemBodiesSync.nebulaeForDim(world.provider.getDimension())); // In hyperspace this same provider serves the transit lanes, and the two things below are // both wrong there: the ring marks a descent boundary in a world nothing descends to, and @@ -172,6 +188,139 @@ public void render(float partialTicks, WorldClient world, Minecraft mc) { restoreState(); } + /** + * Draw the backdrop of this cell's sky — the clouds and the starfield — and return how many clouds + * were emitted. + * + *

The starfield is drawn here and exactly once, between the two cloud passes, because + * where it belongs is the whole point of the ordering and splitting it across two methods is how + * a sky comes to have no stars in it (or two sets of them).

+ * + *

A dark cloud goes AFTER the stars and the other two before them, and that is not a + * flourish: the three appearances are one age sequence, and a molecular cloud is visible precisely + * because it BLOTS OUT what is behind it. Drawn behind the starfield like the other two it would + * paint near-black on black and render as nothing at all — one of the three appearances silently + * missing, which reads as a bug and is indistinguishable from one.

+ */ + private int drawBackdrop(List clouds) { + int drawn = 0; + BufferBuilder buffer = Tessellator.getInstance().getBuffer(); + // Culling OFF while the fans are emitted. They sit on a sphere the camera is INSIDE, which is + // the one case where a winding mistake is silent — the class note above records what that + // costs. A cloud has no facing to get wrong, so the honest fix is to stop asking. + if (clouds != null && !clouds.isEmpty()) { + GlStateManager.disableCull(); + for (PacketSystemBodiesSync.RenderNebula cloud : clouds) { + if (!isDark(cloud)) { + drawn += drawNebula(buffer, cloud) ? 1 : 0; + } + } + GlStateManager.enableCull(); + } + + GlStateManager.color(1.0F, 1.0F, 1.0F, STAR_ALPHA); + GL11.glCallList(this.glStarList); + + if (clouds != null && !clouds.isEmpty()) { + GlStateManager.disableCull(); + for (PacketSystemBodiesSync.RenderNebula cloud : clouds) { + if (isDark(cloud)) { + drawn += drawNebula(buffer, cloud) ? 1 : 0; + } + } + GlStateManager.enableCull(); + } + return drawn; + } + + /** Whether this cloud is the young, thick, star-forming kind — the one that hides what is behind it. */ + private static boolean isDark(PacketSystemBodiesSync.RenderNebula cloud) { + return cloud.appearanceOrdinal == Nebula.Appearance.DARK.ordinal(); + } + + /** + * One cloud: a fan on the sky sphere about its bearing, opaque at the core and fading to nothing + * at the rim. Returns whether anything was emitted. + * + *

The falloff is in the VERTEX COLOURS rather than in a texture, because a nebula's edge is a + * Gaussian with no edge — {@code Nebula.densityAt} says so — and an alpha that reaches zero at the + * rim is what makes the primitive's own boundary invisible. A textured quad would draw a square of + * haze with four corners in it.

+ * + *

Sampled as {@code cosθ·n + sinθ·(cosφ·u + sinφ·v)}, the same construction the atmosphere + * boundary uses and for the same reason: on the sphere there is no singularity, so a viewer INSIDE + * a cloud (θ = 90°) gets a hemisphere of haze rather than a divide-by-zero.

+ */ + private boolean drawNebula(BufferBuilder buffer, PacketSystemBodiesSync.RenderNebula cloud) { + double nx = cloud.dirX; + double ny = cloud.dirY; + double nz = cloud.dirZ; + double len = Math.sqrt(nx * nx + ny * ny + nz * nz); + if (len < 1.0E-6D || cloud.angularRadius <= 0.0F || cloud.opacity <= 0.0F) { + return false; + } + nx /= len; + ny /= len; + nz /= len; + + // Any axis n is not parallel to spans the perpendicular plane with it; take the one it is + // LEAST aligned with, so a cloud lying along a world axis does not degenerate. + double hx = 0.0D, hy = 0.0D, hz = 0.0D; + double ax = Math.abs(nx), ay = Math.abs(ny), az = Math.abs(nz); + if (ax <= ay && ax <= az) { + hx = 1.0D; + } else if (ay <= az) { + hy = 1.0D; + } else { + hz = 1.0D; + } + double ux = ny * hz - nz * hy, uy = nz * hx - nx * hz, uz = nx * hy - ny * hx; + double ul = Math.sqrt(ux * ux + uy * uy + uz * uz); + if (ul < 1.0E-9D) { + return false; + } + ux /= ul; uy /= ul; uz /= ul; + double vx = ny * uz - nz * uy, vy = nz * ux - nx * uz, vz = nx * uy - ny * ux; + + float[] tint = tintOf(cloud); + float alpha = Math.min(NEBULA_MAX_ALPHA, cloud.opacity * NEBULA_MAX_ALPHA); + double theta = Math.min(Math.PI / 2.0D, cloud.angularRadius); + double ct = Math.cos(theta), st = Math.sin(theta); + + buffer.begin(GL11.GL_TRIANGLE_FAN, DefaultVertexFormats.POSITION_COLOR); + buffer.pos(nx * NEBULA_SKY_RADIUS, ny * NEBULA_SKY_RADIUS, nz * NEBULA_SKY_RADIUS) + .color(tint[0], tint[1], tint[2], alpha).endVertex(); + for (int i = 0; i <= NEBULA_SEGMENTS; i++) { + double phi = (Math.PI * 2.0D * i) / NEBULA_SEGMENTS; + double cp = Math.cos(phi), sp = Math.sin(phi); + buffer.pos((ct * nx + st * (cp * ux + sp * vx)) * NEBULA_SKY_RADIUS, + (ct * ny + st * (cp * uy + sp * vy)) * NEBULA_SKY_RADIUS, + (ct * nz + st * (cp * uz + sp * vz)) * NEBULA_SKY_RADIUS) + .color(tint[0], tint[1], tint[2], 0.0F).endVertex(); + } + Tessellator.getInstance().draw(); + return true; + } + + /** + * What a cloud is coloured, by its age. Not a palette choice: the sequence is physical — cold + * molecular gas is nearly black, gas ionised by the stars inside it emits in hydrogen red, and + * what is left once the gas is blown clear is dust reflecting the blue it scatters best. + */ + private static float[] tintOf(PacketSystemBodiesSync.RenderNebula cloud) { + Nebula.Appearance[] looks = Nebula.Appearance.values(); + Nebula.Appearance look = cloud.appearanceOrdinal >= 0 && cloud.appearanceOrdinal < looks.length + ? looks[cloud.appearanceOrdinal] : Nebula.Appearance.REFLECTION; + switch (look) { + case DARK: + return new float[] {0.04F, 0.03F, 0.06F}; + case EMISSION: + return new float[] {0.85F, 0.25F, 0.35F}; + default: + return new float[] {0.35F, 0.50F, 0.90F}; + } + } + /** * Draw {@code body}'s atmosphere boundary: the circle on the sky where its shell meets the * viewer's line of sight. Returns whether anything was emitted. diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index ccbedac08..71756e223 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -3198,7 +3198,99 @@ public long getWorldTimeUniversal(int id) { } } + /** + * {@code space nebulae } — the CLOUD half of a cell's sky, as the server would send + * it: how many are seated in reach, how many survive the render filter, and each one's bearing, + * apparent size, appearance and thickness. + * + *

{@code seated} beside {@code drawn} is the point of the reply. The feed drops clouds too small + * to be a landmark and caps what is left, so "the sky shows two" and "there are two out there" are + * different facts and a test that could not tell them apart would read a working LOD filter as a + * missing cloud.

+ */ + private void handleSpaceNebulae(MinecraftServer server, ICommandSender sender, String[] args) { + zmaster587.advancedRocketry.universe.UniverseRegistry reg = + zmaster587.advancedRocketry.universe.UniverseRegistry.get(server); + if (reg == null) { + send(sender, "{\"error\":\"registry unavailable\"}"); + return; + } + zmaster587.advancedRocketry.space.GalacticCoord cell = + zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal( + parseLongOr(args[1], 0L), parseLongOr(args[2], 0L), parseLongOr(args[3], 0L), + 0L, 0L, 0L); + zmaster587.advancedRocketry.universe.IGalaxyGenerator gen = + zmaster587.advancedRocketry.universe.UniverseRegistry.getGenerator(); + long seed = reg.worldSeed(); + java.util.List drawn = + zmaster587.advancedRocketry.space.SkyNebulaeProducer.around(gen, seed, cell); + int seated = zmaster587.advancedRocketry.space.SkyNebulaeProducer.countAround(gen, seed, cell); + StringBuilder out = new StringBuilder("{\"ok\":true,\"cell\":\""); + out.append(cell.cellKey()).append("\",\"seed\":").append(seed) + .append(",\"seated\":").append(seated) + .append(",\"drawn\":").append(drawn.size()).append(",\"nebulae\":["); + int n = 0; + for (zmaster587.advancedRocketry.network.PacketSystemBodiesSync.RenderNebula cloud : drawn) { + if (n++ > 0) { + out.append(','); + } + out.append("{\"dirX\":").append(cloud.dirX).append(",\"dirY\":").append(cloud.dirY) + .append(",\"dirZ\":").append(cloud.dirZ) + .append(",\"angularRadius\":").append(cloud.angularRadius) + .append(",\"appearance\":").append(cloud.appearanceOrdinal) + .append(",\"opacity\":").append(cloud.opacity).append('}'); + } + out.append("]}"); + send(sender, out.toString()); + } + + /** + * {@code space nebula-find } — walk out along +X from the origin looking for a cell + * whose sky holds a cloud, and report the first one. + * + *

An arrangement helper, and it exists because a cloud's position is a fact about the SEED. A + * test that hard-coded a cell would be pinned to one world's generation and would fail as an + * accusation against the renderer the first time the seed changed; this asks the generator where + * to stand instead. Bounded by {@code steps}, and reports {@code found:false} rather than + * searching forever.

+ */ + private void handleSpaceNebulaFind(MinecraftServer server, ICommandSender sender, String[] args) { + zmaster587.advancedRocketry.universe.UniverseRegistry reg = + zmaster587.advancedRocketry.universe.UniverseRegistry.get(server); + if (reg == null) { + send(sender, "{\"error\":\"registry unavailable\"}"); + return; + } + int steps = Math.max(1, Math.min(4096, parseIntOr(args[1], 64))); + long stride = Math.max(1L, parseLongOr(args[2], 1L)); + zmaster587.advancedRocketry.universe.IGalaxyGenerator gen = + zmaster587.advancedRocketry.universe.UniverseRegistry.getGenerator(); + long seed = reg.worldSeed(); + for (int i = 0; i < steps; i++) { + zmaster587.advancedRocketry.space.GalacticCoord cell = + zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal( + (long) i * stride, 0L, 0L, 0L, 0L, 0L); + java.util.List drawn = + zmaster587.advancedRocketry.space.SkyNebulaeProducer.around(gen, seed, cell); + if (!drawn.isEmpty()) { + send(sender, "{\"ok\":true,\"found\":true,\"cell\":\"" + cell.cellKey() + + "\",\"sectorX\":" + cell.sectorX() + ",\"drawn\":" + drawn.size() + + ",\"largest\":" + drawn.get(0).angularRadius + ",\"steps\":" + i + "}"); + return; + } + } + send(sender, "{\"ok\":true,\"found\":false,\"searched\":" + steps + ",\"stride\":" + stride + "}"); + } + private void handleSpace(MinecraftServer server, ICommandSender sender, String[] args) { + if (args.length >= 4 && "nebulae".equalsIgnoreCase(args[0])) { + handleSpaceNebulae(server, sender, args); + return; + } + if (args.length >= 3 && "nebula-find".equalsIgnoreCase(args[0])) { + handleSpaceNebulaFind(server, sender, args); + return; + } // --- PRODUCTION-wiring probes. Unlike every other verb here these deliberately touch the real // SpaceSubsystem rather than a probe-local stack, so a restart test can prove the shipped // server-start / world-save path actually persists and restores. They are only useful when diff --git a/src/main/java/zmaster587/advancedRocketry/network/PacketSystemBodiesSync.java b/src/main/java/zmaster587/advancedRocketry/network/PacketSystemBodiesSync.java index 284769e82..ac7ebdf21 100644 --- a/src/main/java/zmaster587/advancedRocketry/network/PacketSystemBodiesSync.java +++ b/src/main/java/zmaster587/advancedRocketry/network/PacketSystemBodiesSync.java @@ -32,9 +32,18 @@ * {@code writeInt(slotDimId)}, {@code writeInt(bodyCount)} and, per body, * {@code writeInt(kindOrdinal)}, {@code writeLong(localX)}, {@code writeLong(localY)}, * {@code writeLong(localZ)}, {@code writeInt(dimId)}, {@code writeBoolean(descendTarget)}, - * {@code writeLong(boundaryRadius)}. - * {@code executeClient} stashes the decoded payload into a client-side static map (idempotent overwrite) - * that {@link #bodiesForDim(int)} reads; {@code read} and {@code executeServer} are never used.

+ * {@code writeLong(boundaryRadius)}; then the NEBULA half, {@code writeInt(dimCount)} and, per dim, + * {@code writeInt(slotDimId)}, {@code writeInt(nebulaCount)} and, per cloud, + * {@code writeFloat(dirX/dirY/dirZ)}, {@code writeFloat(angularRadius)}, + * {@code writeInt(appearanceOrdinal)}, {@code writeFloat(opacity)}. + * {@code executeClient} stashes the decoded payload into client-side static maps (idempotent overwrite) + * that {@link #bodiesForDim(int)} and {@link #nebulaeForDim(int)} read; {@code read} and + * {@code executeServer} are never used.

+ * + *

The nebula half rides this packet rather than one of its own because it answers the same question + * — what does the sky of this cell show — keyed by the same cell→slot binding and cleared by the + * same empty payload. Bodies carry a POSITION (they are destinations); a cloud carries a DIRECTION and + * an apparent size, and nothing else, because it is not one.

*/ public final class PacketSystemBodiesSync extends BasePacket { @@ -76,17 +85,76 @@ public String toString() { } } + /** + * One nebula for a slot dim: a DIRECTION and an apparent SIZE, never a position. + * + *

A cloud is light years across and hundreds of light years away, so it has no parallax across + * a cell and nothing can be flown to it — it is deliberately not a destination and carries no + * address. What the sky needs is where to look, how much of the sky it covers, what it looks + * like, and how thick it is; those four are all of it.

+ */ + public static final class RenderNebula { + /** Unit vector from the observer towards the cloud's centre, in the static frame. */ + public final float dirX; + public final float dirY; + public final float dirZ; + /** + * Half-angle the cloud subtends, in radians. A viewer INSIDE one gets a right angle: the + * cloud is all around him, which is the honest limit rather than an overflow. + */ + public final float angularRadius; + /** {@code Nebula.Appearance} ordinal — dark, emission or reflection. Decides the tint. */ + public final int appearanceOrdinal; + /** How thick it is at its densest, {@code 0}..{@code 1}. Decides how strongly it draws. */ + public final float opacity; + + public RenderNebula(float dirX, float dirY, float dirZ, float angularRadius, + int appearanceOrdinal, float opacity) { + this.dirX = dirX; + this.dirY = dirY; + this.dirZ = dirZ; + this.angularRadius = angularRadius; + this.appearanceOrdinal = appearanceOrdinal; + this.opacity = opacity; + } + + @Override + public String toString() { + return "RenderNebula{dir=" + dirX + "," + dirY + "," + dirZ + ",theta=" + angularRadius + + ",look=" + appearanceOrdinal + ",opacity=" + opacity + "}"; + } + } + /** Client-side render store: slot dim id -> bodies to draw. Read by the sky renderer via {@link #bodiesForDim}. */ private static final Map> CLIENT_BODIES = new LinkedHashMap<>(); + /** Client-side render store: slot dim id -> nebulae to draw. Read via {@link #nebulaeForDim}. */ + private static final Map> CLIENT_NEBULAE = new LinkedHashMap<>(); + /** The decoded payload carried by this instance (server: what to send; client: what was received). */ private Map> byDim = new LinkedHashMap<>(); + /** The nebula half of the same payload, keyed the same way. */ + private Map> nebulaeByDim = new LinkedHashMap<>(); + public PacketSystemBodiesSync() { } /** Server factory: snapshot the per-slot-dim render bodies to broadcast to a client. */ public static PacketSystemBodiesSync forDims(Map> byDim) { + return forDims(byDim, null); + } + + /** + * Server factory carrying BOTH halves of a cell's sky. + * + *

One channel and not two, because both are answers to the same question — what does the sky of + * this cell show — keyed by the same cell→slot binding, cleared by the same empty payload and + * broadcast on the same tick. A second channel would be a second lifecycle to keep in step, and the + * two skies could then disagree about which cell the viewer is in.

+ */ + public static PacketSystemBodiesSync forDims(Map> byDim, + Map> nebulaeByDim) { PacketSystemBodiesSync p = new PacketSystemBodiesSync(); if (byDim != null) { for (Map.Entry> e : byDim.entrySet()) { @@ -96,6 +164,14 @@ public static PacketSystemBodiesSync forDims(Map> byDi p.byDim.put(e.getKey(), bodies); } } + if (nebulaeByDim != null) { + for (Map.Entry> e : nebulaeByDim.entrySet()) { + List clouds = e.getValue() == null + ? new ArrayList() + : new ArrayList<>(e.getValue()); + p.nebulaeByDim.put(e.getKey(), clouds); + } + } return p; } @@ -109,6 +185,11 @@ public Map> payload() { return byDim; } + /** The nebula half of the decoded payload of THIS instance. */ + public Map> nebulaPayload() { + return nebulaeByDim; + } + @Override public void write(ByteBuf out) { PacketBuffer buffer = new PacketBuffer(out); @@ -127,6 +208,20 @@ public void write(ByteBuf out) { buffer.writeLong(b.boundaryRadius); } } + buffer.writeInt(nebulaeByDim.size()); + for (Map.Entry> e : nebulaeByDim.entrySet()) { + List clouds = e.getValue(); + buffer.writeInt(e.getKey()); + buffer.writeInt(clouds.size()); + for (RenderNebula n : clouds) { + buffer.writeFloat(n.dirX); + buffer.writeFloat(n.dirY); + buffer.writeFloat(n.dirZ); + buffer.writeFloat(n.angularRadius); + buffer.writeInt(n.appearanceOrdinal); + buffer.writeFloat(n.opacity); + } + } } @Override @@ -152,6 +247,26 @@ public void readClient(ByteBuf in) { decoded.put(slotDimId, bodies); } byDim = decoded; + + Map> decodedClouds = new LinkedHashMap<>(); + int cloudDimCount = buffer.readInt(); + for (int i = 0; i < cloudDimCount; i++) { + int slotDimId = buffer.readInt(); + int cloudCount = buffer.readInt(); + List clouds = new ArrayList<>(); + for (int j = 0; j < cloudCount; j++) { + float dirX = buffer.readFloat(); + float dirY = buffer.readFloat(); + float dirZ = buffer.readFloat(); + float angularRadius = buffer.readFloat(); + int appearanceOrdinal = buffer.readInt(); + float opacity = buffer.readFloat(); + clouds.add(new RenderNebula(dirX, dirY, dirZ, angularRadius, appearanceOrdinal, + opacity)); + } + decodedClouds.put(slotDimId, clouds); + } + nebulaeByDim = decodedClouds; } @Override @@ -164,6 +279,8 @@ public void read(ByteBuf in) { public void executeClient(EntityPlayer player) { CLIENT_BODIES.clear(); CLIENT_BODIES.putAll(byDim); + CLIENT_NEBULAE.clear(); + CLIENT_NEBULAE.putAll(nebulaeByDim); } @Override @@ -176,4 +293,11 @@ public static List bodiesForDim(int slotDimId) { List bodies = CLIENT_BODIES.get(slotDimId); return bodies == null ? Collections.emptyList() : bodies; } + + /** Client render read: the nebulae to draw in {@code slotDimId}. Never null. */ + @SideOnly(Side.CLIENT) + public static List nebulaeForDim(int slotDimId) { + List clouds = CLIENT_NEBULAE.get(slotDimId); + return clouds == null ? Collections.emptyList() : clouds; + } } diff --git a/src/main/java/zmaster587/advancedRocketry/space/SkyNebulaeProducer.java b/src/main/java/zmaster587/advancedRocketry/space/SkyNebulaeProducer.java new file mode 100644 index 000000000..8635b6c87 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/space/SkyNebulaeProducer.java @@ -0,0 +1,223 @@ +package zmaster587.advancedRocketry.space; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import zmaster587.advancedRocketry.network.PacketSystemBodiesSync.RenderNebula; +import zmaster587.advancedRocketry.universe.IGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Nebula; +import zmaster587.advancedRocketry.universe.UniverseRegistry; +import zmaster587.advancedRocketry.universe.UniverseScale; + +/** + * Server-side producer for the nebula half of the cell sky: turns the clouds seated around a cell into + * the DIRECTIONS and apparent SIZES a client draws. + * + *

A cloud is the only landmark the universe layer has. A star cluster is invisible from outside it — + * it can be identified only by counting stars, which no player will ever do — and a cloud is the thing + * that makes a region recognisable at a glance, from a long way off. That is what this feed is for.

+ * + *

A direction, never a position

+ *

A cloud is light years across and hundreds of light years away, so it does not move on the sky when + * a ship crosses a cell: a cell is about 4·10⁻⁴ light years. Its bearing is therefore computed from the + * CELL and not from a ship inside it, unlike the bodies beside it, and nothing here is a place that can + * be flown to — a nebula has no cell name by design (attribution reads names, not matter).

+ * + *

What is dropped, and it is not silent

+ *

Two bounds, both LOD and both stated: a cloud smaller than {@link #MIN_ANGULAR_RADIUS} on the sky + * is a smudge and is left out, and at most {@link #MAX_PER_CELL} are sent, largest first. The cap + * drops the SMALLEST, so what is lost is always what would have been least visible — but a caller that + * needs to know how much was dropped can compare against {@link #countAround}.

+ */ +public final class SkyNebulaeProducer { + + /** + * How far out clouds are gathered, in light years. A cluster lattice cell is 300 ly, so this is a + * few cluster cells each way; a cloud tens of light years across still subtends more than a degree + * at this range, and past it the angular filter below would drop it anyway. + */ + public static final double SKY_REACH_LY = 1_000d; + + /** + * The smallest a cloud may look and still be worth drawing, in radians (~0.6°, a little wider than + * the Moon from Earth). Below it a nebula is a few pixels of haze that cannot be a landmark. + */ + public static final double MIN_ANGULAR_RADIUS = 0.01d; + + /** How many clouds one cell's sky may carry. Largest first; the sky is a backdrop, not a catalogue. */ + public static final int MAX_PER_CELL = 12; + + /** + * What each cell's sky showed last time it was asked, keyed {@code seed|cellKey}. + * + *

Derived data and never a dependency: every entry can be recomputed from {@code (seed, cell)} + * alone, and {@link #reset()} restores the empty map rather than nulling anything. It exists + * because the answer is CONSTANT — a cloud is hundreds of light years away and a cell is 4·10⁻⁴ of + * one across, so re-deriving it once a second per loaded cell would burn a few thousand hashes and + * a heap of short-lived clusters to arrive at the same list.

+ */ + private static final Map> CACHE = new LinkedHashMap<>(); + + /** How many cells the cache keeps. Oldest out first; a pool of live cells is far smaller than this. */ + private static final int CACHE_LIMIT = 64; + + private SkyNebulaeProducer() { + } + + /** Drop the per-cell cache (server stop, or a generator/seed change under a test). */ + public static void reset() { + synchronized (CACHE) { + CACHE.clear(); + } + } + + /** + * The clouds visible from {@code cell}, as render records, largest first. + * + * @param generator the seam the clouds come from; a generator with no clusters answers empty + * @param seed the world seed the generator is deterministic in + */ + public static List around(IGalaxyGenerator generator, long seed, + GalacticCoord cell) { + if (generator == null || cell == null) { + return Collections.emptyList(); + } + List found = generator.nebulaeAround(seed, cell, SKY_REACH_LY); + if (found == null || found.isEmpty()) { + return Collections.emptyList(); + } + GalacticCoord c = cell.cellCentre(); + double observerX = UniverseScale.lightYearsForCells(c.sectorX()); + double observerY = UniverseScale.lightYearsForCells(c.sectorY()); + double observerZ = UniverseScale.lightYearsForCells(c.sectorZ()); + + List out = new ArrayList<>(); + for (Nebula nebula : found) { + RenderNebula drawn = renderOf(nebula, observerX, observerY, observerZ); + if (drawn != null) { + out.add(drawn); + } + } + // Largest first, so the cap below can only ever drop the least visible. + Collections.sort(out, new Comparator() { + @Override + public int compare(RenderNebula a, RenderNebula b) { + return Float.compare(b.angularRadius, a.angularRadius); + } + }); + return out.size() <= MAX_PER_CELL ? out : new ArrayList<>(out.subList(0, MAX_PER_CELL)); + } + + /** How many clouds are seated in reach of {@code cell} before any LOD filter — what was dropped. */ + public static int countAround(IGalaxyGenerator generator, long seed, GalacticCoord cell) { + if (generator == null || cell == null) { + return 0; + } + List found = generator.nebulaeAround(seed, cell, SKY_REACH_LY); + return found == null ? 0 : found.size(); + } + + /** + * One cloud as seen from an observer, or {@code null} when it is too small on the sky to draw. + * + *

The half-angle is {@code asin(radius / distance)}, so a cloud OPENS as a ship closes on it, and + * a viewer inside one gets a right angle — the cloud is all around him, which is the honest limit + * rather than an overflow. The direction is then arbitrary and the sky is filled either way, so the + * degenerate zero-distance case keeps a fixed axis instead of a NaN.

+ */ + public static RenderNebula renderOf(Nebula nebula, double observerXLy, double observerYLy, + double observerZLy) { + if (nebula == null) { + return null; + } + double dx = nebula.centreXLy() - observerXLy; + double dy = nebula.centreYLy() - observerYLy; + double dz = nebula.centreZLy() - observerZLy; + double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + double angularRadius; + double nx; + double ny; + double nz; + if (distance <= nebula.radiusLy()) { + // Inside it: the cloud fills the sky, and which way its centre lies stops mattering. + angularRadius = Math.PI / 2d; + double length = distance < 1.0E-9d ? 0d : distance; + nx = length == 0d ? 0d : dx / length; + ny = length == 0d ? 1d : dy / length; + nz = length == 0d ? 0d : dz / length; + } else { + angularRadius = Math.asin(nebula.radiusLy() / distance); + if (angularRadius < MIN_ANGULAR_RADIUS) { + return null; + } + nx = dx / distance; + ny = dy / distance; + nz = dz / distance; + } + return new RenderNebula((float) nx, (float) ny, (float) nz, (float) angularRadius, + nebula.appearance().ordinal(), (float) nebula.peakDensity()); + } + + /** + * The clouds of every materialized cell, keyed by the slot dim that cell is bound to — the same + * keying the bodies beside them use, and read from the same bindings. + * + *

A live cell with no cloud gets a present-and-EMPTY entry, exactly as the bodies feed does: + * "present and empty" is what clears a stale sky, where "absent" would leave one standing.

+ */ + public static Map> buildByDim(Map loadedCells, + IGalaxyGenerator generator, long seed) { + Map> byDim = new LinkedHashMap<>(); + if (loadedCells == null) { + return byDim; + } + for (Map.Entry bound : loadedCells.entrySet()) { + Integer slotDim = bound.getValue(); + GalacticCoord cell = GalacticCoord.fromCellKey(bound.getKey()); + if (slotDim == null || slotDim == SpaceManager.UNBOUND_SLOT || cell == null) { + continue; + } + byDim.put(slotDim, cached(generator, seed, cell)); + } + return byDim; + } + + /** {@link #around} through the per-cell cache. */ + private static List cached(IGalaxyGenerator generator, long seed, + GalacticCoord cell) { + String key = seed + "|" + cell.cellCentre().cellKey(); + synchronized (CACHE) { + List hit = CACHE.get(key); + if (hit != null) { + return hit; + } + } + List computed = around(generator, seed, cell); + synchronized (CACHE) { + if (CACHE.size() >= CACHE_LIMIT) { + java.util.Iterator oldest = CACHE.keySet().iterator(); + if (oldest.hasNext()) { + oldest.next(); + oldest.remove(); + } + } + CACHE.put(key, computed); + } + return computed; + } + + /** The live per-slot-dim clouds from the production bindings + the installed generator. */ + public static Map> currentByDim(net.minecraft.server.MinecraftServer server) { + UniverseRegistry reg = UniverseRegistry.get(server); + SpaceManager space = SpaceSubsystem.space(); + if (reg == null || space == null) { + return new LinkedHashMap<>(); + } + return buildByDim(space.loadedCells(), UniverseRegistry.getGenerator(), reg.worldSeed()); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java b/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java index fa24de35b..8e770bff9 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java +++ b/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java @@ -8,6 +8,7 @@ import zmaster587.advancedRocketry.api.Constants; import zmaster587.advancedRocketry.network.PacketSystemBodiesSync; import zmaster587.advancedRocketry.network.PacketSystemBodiesSync.RenderBody; +import zmaster587.advancedRocketry.network.PacketSystemBodiesSync.RenderNebula; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; import zmaster587.advancedRocketry.universe.UniverseRegistry; @@ -199,7 +200,8 @@ public static Map> currentByDim(MinecraftServer server /** Build the live packet from the production cell bindings + universe registry, or an empty packet. */ public static PacketSystemBodiesSync currentPacket(MinecraftServer server) { - return PacketSystemBodiesSync.forDims(currentByDim(server)); + return PacketSystemBodiesSync.forDims(currentByDim(server), + SkyNebulaeProducer.currentByDim(server)); } /** @@ -215,7 +217,8 @@ public static PacketSystemBodiesSync currentPacket(MinecraftServer server) { * stale sky, where an absent one would leave it standing. A player who is not in a slot world at * all is sent nothing.

*/ - private static void broadcastTo(EntityPlayerMP player, Map> byDim) { + private static void broadcastTo(EntityPlayerMP player, Map> byDim, + Map> nebulaeByDim) { if (player == null) { return; } @@ -227,9 +230,15 @@ private static void broadcastTo(EntityPlayerMP player, Map clouds = nebulaeByDim == null ? null : nebulaeByDim.get(dim); Map> one = new LinkedHashMap<>(); one.put(dim, bodies); - PacketHandler.sendToPlayer(PacketSystemBodiesSync.forDims(one), player); + Map> oneSky = new LinkedHashMap<>(); + oneSky.put(dim, clouds == null ? Collections.emptyList() : clouds); + PacketHandler.sendToPlayer(PacketSystemBodiesSync.forDims(one, oneSky), player); } /** Login send: give a joining player the sky of the dimension he arrived in. */ @@ -238,7 +247,8 @@ public static void sendToPlayer(EntityPlayerMP player) { return; } try { - broadcastTo(player, currentByDim(FMLCommonHandler.instance().getMinecraftServerInstance())); + MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance(); + broadcastTo(player, currentByDim(server), SkyNebulaeProducer.currentByDim(server)); } catch (Throwable t) { AdvancedRocketry.logger.warn("[SPACE] system-bodies login send failed", t); } @@ -258,16 +268,18 @@ public static void onBroadcastTick(MinecraftServer server) { } try { Map> byDim = currentByDim(server); + Map> nebulaeByDim = SkyNebulaeProducer.currentByDim(server); for (EntityPlayerMP player : server.getPlayerList().getPlayers()) { - broadcastTo(player, byDim); + broadcastTo(player, byDim, nebulaeByDim); } } catch (Throwable t) { AdvancedRocketry.logger.warn("[SPACE] system-bodies broadcast failed", t); } } - /** Reset the broadcast cadence (server stop). */ + /** Reset the broadcast cadence and the sky's derived caches (server stop). */ public static void reset() { tickCounter = 0; + SkyNebulaeProducer.reset(); } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 26ca8df81..4f3413190 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -557,6 +557,33 @@ public BodyProfile profileOf(long seed, GalacticCoord anchor, SystemBody body, S body.kind() == SystemBodyKind.MOON, body.orbitalDistance()); } + /** + * {@inheritDoc} + * + *

Cost is the number of CLUSTER cells the reach crosses, not its volume in cells — the clouds + * are enumerated on the cluster lattice they are derived from. Outside a galaxy the answer is + * empty by construction: clusters are seated inside galaxies, and a cloud is a cluster's own gas.

+ */ + @Override + public List nebulaeAround(long seed, GalacticCoord cell, double radiusLy) { + if (cell == null || !(radiusLy > 0d)) { + return Collections.emptyList(); + } + GalacticCoord c = cell.cellCentre(); + Optional galaxy = galaxies.galaxyOwningSector(seed, c.sectorX(), c.sectorY(), + c.sectorZ()); + if (!galaxy.isPresent()) { + return Collections.emptyList(); + } + long s = config.minSpacing; + long reachSuper = Math.max(1L, UniverseScale.cellsForLightYears(radiusLy) / s); + long supX = Math.floorDiv(c.sectorX(), s); + long supY = Math.floorDiv(c.sectorY(), s); + long supZ = Math.floorDiv(c.sectorZ(), s); + return nebulae.nebulaeInRegion(seed, galaxy.get(), supX - reachSuper, supY - reachSuper, + supZ - reachSuper, supX + reachSuper, supY + reachSuper, supZ + reachSuper); + } + @Override public Optional anchorAt(long seed, GalacticCoord cell) { Optional g = systemForLattice(seed, diff --git a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java index 9f3992623..dccde502b 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java @@ -58,6 +58,18 @@ default Optional anchorAt(long seed, GalacticCoord cell) { return systemAt(seed, cell).isPresent() ? Optional.of(cell.cellCentre()) : Optional.empty(); } + /** + * The nebulae seated within {@code radiusLy} light years of {@code cell} — what a sky asks, because + * a cloud is meant to be seen from OUTSIDE it. + * + *

A DIRECTION-and-size query, never a placement one: a nebula has no cell name and is not a + * body, so nothing here can be flown to. The default is empty, which is the correct answer for a + * generator with no clusters rather than a stub — no clusters means no gas.

+ */ + default List nebulaeAround(long seed, GalacticCoord cell, double radiusLy) { + return Collections.emptyList(); + } + /** * The super-cell edge (in cells) this generator partitions space by — at most one system per * {@code minSpacingCells}-cube. The registry uses it to attribute member cells of AUTHORED systems and diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java index 8e3247f9d..398af76d1 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java @@ -452,6 +452,95 @@ public void aPilotInASlotCellSeesTheBodiesAndStars() throws Exception { descendTargets, boundariesWithBodies); } + /** + * A pilot in a cell near a molecular cloud sees the cloud. + * + *

A star cluster is invisible from outside it — it can be told apart only by counting its stars, + * which nobody will do — so the cloud wrapping it is the one landmark the universe layer has. This + * measures whether it reaches the screen at all.

+ * + *

Counted, not photographed, and that is deliberate. A nebula is haze whose alpha falls to + * zero at its rim; a pixel-difference test would be measuring the tuning of {@code NEBULA_MAX_ALPHA} + * as much as the feed, and would go red the first time the haze was made subtler. The renderer's own + * per-frame counter answers "did a cloud reach the rasterizer" exactly. It is read BESIDE + * {@code skyFramesDrawn}, because a zero means "no cloud was drawn" only if the sky renderer ran at + * all — the two are different questions and one counter cannot tell them apart.

+ * + *

Where the cloud is comes from the SERVER, not from this test. A cloud's position is a + * fact about the seed; a hard-coded cell would pin this test to one world's generation and would + * fail as an accusation against the renderer the first time the seed moved. The probe is asked where + * to stand.

+ */ + @Test + public void aPilotNearACloudSeesIt() throws Exception { + JsonObject rd = bot().setRenderDistance(SKY_RENDER_DISTANCE); + int previousRenderDistance = rd.get("previous").getAsInt(); + assertTrue("the sky pass gate must be open, read back off the client's own field: " + rd, + rd.get("skyPassEnabled").getAsBoolean()); + String health = exec("artest player health"); + Matcher nameM = PLAYER_NAME.matcher(health); + assertTrue("player health must echo the player name: " + health, nameM.find()); + botName = nameM.group(1); + try { + String setup = exec("artest space entry-setup 1"); + assertTrue("entry-setup must install the stack: " + setup, setup.contains("\"ok\":true")); + + // A universe with clusters in it. Without a world has no galaxies, hence no + // clusters, hence no gas — and an empty sky would be honest for the wrong reason. + String gen = exec("artest space gen-install 0.9 8"); + assertTrue("the procedural generator must install: " + gen, gen.contains("\"ok\":true")); + + String found = exec("artest space nebula-find 512 64"); + assertTrue("the generator must be able to name a cell with a cloud in reach: " + found, + found.contains("\"found\":true")); + Matcher sectorM = Pattern.compile("\"sectorX\":(-?\\d+)").matcher(found); + assertTrue("the find must report the cell it found: " + found, sectorM.find()); + String cloudCell = sectorM.group(1) + " 0 0"; + + String settle = exec("artest space ledger-settle " + cloudCell + " 0"); + assertTrue("ledger-settle must succeed: " + settle, settle.contains("\"ok\":true")); + Matcher boundM = BOUND_DIM.matcher(settle); + assertTrue("the settle must report which slot the cell was bound to: " + settle, + boundM.find()); + int slotDim = Integer.parseInt(boundM.group(1)); + + // The server's own answer for that cell, as the cross-side oracle: what it will send. + String feed = exec("artest space nebulae " + cloudCell); + Matcher drawnM = Pattern.compile("\"drawn\":(\\d+)").matcher(feed); + assertTrue("the probe must report the cell's sky: " + feed, drawnM.find()); + int serverClouds = Integer.parseInt(drawnM.group(1)); + assertTrue("the cell the finder chose must actually have a cloud in its sky: " + feed, + serverClouds >= 1); + + exec("time set 18000"); + seat(slotDim, CELL_CAPTURE_Y); + + // Gate on the FEED reaching the client, then on a frame being drawn after it did. Waiting + // a fixed number of ticks would make a slow broadcast read as a renderer that draws nothing. + int drawn = 0; + long frames = 0L; + for (int attempt = 0; attempt < 30 && drawn == 0; attempt++) { + bot().waitTicks(10); + frames = Long.parseLong(bot().readStaticField(SKY_CLASS, "skyFramesDrawn") + .get("value").getAsString().trim()); + drawn = skyCounter("nebulaeDrawnLastFrame"); + } + + assertTrue("HARNESS CONTROL: the sky renderer never ran, so nothing below could mean" + + " anything (frames=" + frames + ")", frames > 0L); + assertTrue("the server had " + serverClouds + " cloud(s) in this cell's sky and the client" + + " drew " + drawn + ": a landmark that reaches the feed and not the frame is a" + + " landmark nobody can navigate by", drawn >= 1); + } finally { + try { + exec("artest space gen-reset"); + } catch (Exception ignored) { + // the generator is a JVM global: a shared client run must not inherit this one + } + bot().setRenderDistance(previousRenderDistance); + } + } + /** How many body labels the client's last rendered frame wrote. */ private int labelsDrawn() throws Exception { return skyCounter("labelsDrawnLastFrame"); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/NebulaSkyFeedE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/NebulaSkyFeedE2ETest.java new file mode 100644 index 000000000..aef84391a --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/server/NebulaSkyFeedE2ETest.java @@ -0,0 +1,102 @@ +package zmaster587.advancedRocketry.test.server; + +import com.github.stannismod.forge.testing.junit.AbstractHeadlessServerTest; + +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * What the server tells a cell's sky about the clouds around it, driven on a real server. + * + *

The unit tier pins the geometry — which way a cloud lies, how big it looks, what is filtered out. + * This pins the thing that tier cannot see: that a real generator in a real world actually SEATS + * clouds, and that the reply a client would be sent is derived from that world's own seed rather than + * from anything a test arranged.

+ * + *

Per-method harness on purpose: this installs a procedural generator, which is a JVM-global, and a + * shared server would carry it into every class that ran after it.

+ */ +public class NebulaSkyFeedE2ETest extends AbstractHeadlessServerTest { + + /** A dense galaxy so a bounded sweep finds a cluster, at the shipped star spacing. */ + private static final String GEN_INSTALL = "artest space gen-install 0.9 8 987654321"; + + private String exec(String cmd) throws Exception { + return String.join("\n", client().execute(cmd)); + } + + @After + public void restoreGenerator() throws Exception { + try { + exec("artest space gen-reset"); + } catch (Exception ignored) { + } + } + + private static long field(String json, String name) { + String key = "\"" + name + "\":"; + int at = json.indexOf(key); + assertTrue("probe reply has no field " + name + ": " + json, at >= 0); + int from = at + key.length(); + int to = from; + while (to < json.length() && "-0123456789".indexOf(json.charAt(to)) >= 0) { + to++; + } + return Long.parseLong(json.substring(from, to)); + } + + @Test + public void aGalaxyWithClustersInItHasCloudsToLookAt() throws Exception { + String installed = exec(GEN_INSTALL); + assertTrue("the procedural generator must install: " + installed, + installed.contains("\"ok\":true")); + + String found = exec("artest space nebula-find 512 64"); + assertTrue("a dense galaxy must have a cloud somewhere in it: " + found, + found.contains("\"found\":true")); + + long sectorX = field(found, "sectorX"); + String feed = exec("artest space nebulae " + sectorX + " 0 0"); + assertTrue("the cell the finder named must report its sky: " + feed, feed.contains("\"ok\":true")); + assertTrue("and that sky must hold the cloud the finder found: " + feed, + field(feed, "drawn") >= 1); + assertTrue("a cloud that is drawn must cover something of the sky: " + feed, + feed.contains("\"angularRadius\":")); + } + + @Test + public void withoutAProceduralGeneratorTheSkyIsEmptyRatherThanInvented() throws Exception { + // The negative leg, and it is the one that matters: an authored-only pack has no galaxies, so + // it has no clusters and no gas. A feed that produced a cloud here would be producing it from + // nothing — and a landmark nobody generated is worse than no landmark. + String reset = exec("artest space gen-reset"); + assertTrue("the default generator must be restorable: " + reset, reset.contains("\"ok\":true")); + + String feed = exec("artest space nebulae 0 0 0"); + assertTrue("the probe must still answer: " + feed, feed.contains("\"ok\":true")); + assertEquals("a universe with no clusters must seat no clouds: " + feed, 0L, + field(feed, "seated")); + assertEquals("and must draw none: " + feed, 0L, field(feed, "drawn")); + } + + @Test + public void whatIsSeatedAndWhatIsDrawnAreReportedSeparately() throws Exception { + // So a reader can tell a working level-of-detail filter from a missing cloud. Without the two + // numbers side by side, "the sky shows one" and "there is one out there" are the same reading, + // and a filter doing its job would be indistinguishable from a generator that stopped seating. + String installed = exec(GEN_INSTALL); + assertTrue("the procedural generator must install: " + installed, + installed.contains("\"ok\":true")); + + String found = exec("artest space nebula-find 512 64"); + assertTrue("a dense galaxy must have a cloud somewhere in it: " + found, + found.contains("\"found\":true")); + String feed = exec("artest space nebulae " + field(found, "sectorX") + " 0 0"); + + assertTrue("what is drawn may never exceed what is seated: " + feed, + field(feed, "drawn") <= field(feed, "seated")); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java new file mode 100644 index 000000000..b2560d788 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java @@ -0,0 +1,169 @@ +package zmaster587.advancedRocketry.test.unit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.Test; + +import zmaster587.advancedRocketry.network.PacketSystemBodiesSync.RenderNebula; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.space.SkyNebulaeProducer; +import zmaster587.advancedRocketry.universe.IGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Nebula; +import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.UniverseScale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * What the sky is told about the clouds around a cell. + * + *

These pin the promises a viewer can check: a cloud lies in the direction it really lies in, it + * LOOKS bigger from closer, a viewer inside one has it all around him, a cloud too small to be a + * landmark is left out rather than drawn as a speck, and a generator with no clusters produces an + * empty sky rather than a fabricated one. They do not pin the reach, the filter threshold or the cap + * — those are render tunables and moving them must not turn a test red.

+ */ +public class SkyNebulaeProducerTest { + + /** A cloud seated at a stated point, with a stated size. The cluster behind it is not read here. */ + private static Nebula cloudAt(double xLy, double yLy, double zLy, double radiusLy) { + return new Nebula(null, Nebula.Appearance.EMISSION, xLy, yLy, zLy, radiusLy, 0.8d); + } + + /** A generator that answers with exactly these clouds, whatever is asked. */ + private static IGalaxyGenerator generatorOf(final List clouds) { + return new IGalaxyGenerator() { + @Override + public Optional systemAt(long seed, GalacticCoord coord) { + return Optional.empty(); + } + + @Override + public Map systemsInRegion(long seed, GalacticCoord min, + GalacticCoord max) { + return Collections.emptyMap(); + } + + @Override + public List nebulaeAround(long seed, GalacticCoord cell, double radiusLy) { + return new ArrayList<>(clouds); + } + }; + } + + /** The cell whose centre sits {@code ly} light years out along +X. */ + private static GalacticCoord cellAtLightYears(double ly) { + return GalacticCoord.ofSectorLocal(UniverseScale.cellsAt(ly), 0L, 0L, 0L, 0L, 0L); + } + + @Test + public void aCloudLiesInTheDirectionItReallyLies() { + // The one thing a landmark has to get right: look that way and it is there. + List sky = SkyNebulaeProducer.around( + generatorOf(Arrays.asList(cloudAt(0d, 0d, 200d, 40d))), 1L, GalacticCoord.ORIGIN); + + assertEquals("the one cloud in reach must be in the sky", 1, sky.size()); + RenderNebula drawn = sky.get(0); + assertEquals("a cloud straight along +Z must be drawn straight along +Z", 1.0F, drawn.dirZ, 1.0E-4F); + assertEquals(0.0F, drawn.dirX, 1.0E-4F); + assertEquals(0.0F, drawn.dirY, 1.0E-4F); + } + + @Test + public void aCloudLooksBiggerFromCloser() { + // What makes it a landmark rather than a decal: it opens as you close on it, so a pilot can + // tell whether he is approaching one. + // The cloud sits along +X because that is the axis the observer moves along; put it anywhere + // else and stepping "closer" walks past it, which is what the first version of this did. + List one = Arrays.asList(cloudAt(400d, 0d, 0d, 50d)); + float far = SkyNebulaeProducer.around(generatorOf(one), 1L, GalacticCoord.ORIGIN) + .get(0).angularRadius; + float near = SkyNebulaeProducer.around(generatorOf(one), 1L, cellAtLightYears(200d)) + .get(0).angularRadius; + + assertTrue("a cloud must subtend more from closer: far=" + far + " near=" + near, near > far); + } + + @Test + public void insideACloudItIsAllAroundYou() { + // The honest limit rather than an overflow: at zero distance the half-angle would diverge if + // it were computed on a plane, and a ship that flew into a cloud would see a NaN-sized hole. + RenderNebula inside = SkyNebulaeProducer.renderOf(cloudAt(0d, 0d, 10d, 100d), 0d, 0d, 0d); + + assertNotNull("a viewer inside a cloud still has a sky", inside); + assertEquals("and the cloud fills half of it", (float) (Math.PI / 2d), inside.angularRadius, + 1.0E-4F); + } + + @Test + public void aCloudTooSmallToBeALandmarkIsNotDrawn() { + // The LOD rule, stated as the thing it protects: a few pixels of haze is not a landmark, and + // drawing it costs a fan for something nobody can navigate by. + double farAway = 100_000d; + assertNull("a distant speck must be left out", + SkyNebulaeProducer.renderOf(cloudAt(0d, 0d, farAway, 1d), 0d, 0d, 0d)); + assertNotNull("while the same cloud near enough to see must not be", + SkyNebulaeProducer.renderOf(cloudAt(0d, 0d, 50d, 1d), 0d, 0d, 0d)); + } + + @Test + public void aGeneratorWithNoCloudsGivesAnEmptySkyAndNotAFabricatedOne() { + // The negative case the whole feed has to keep: a universe with no clusters has no gas, and + // an empty sky must stay empty rather than acquire a default cloud. + assertTrue("void must yield no clouds", + SkyNebulaeProducer.around(generatorOf(Collections.emptyList()), 1L, + GalacticCoord.ORIGIN).isEmpty()); + assertTrue("and so must no generator at all", + SkyNebulaeProducer.around(null, 1L, GalacticCoord.ORIGIN).isEmpty()); + } + + @Test + public void theSkyIsOrderedLargestFirstSoTheCapDropsTheLeastVisible() { + // The cap is a bound on work, and a bound on work must never decide WHICH landmark survives + // by accident of enumeration order. + List many = new ArrayList<>(); + for (int i = 1; i <= SkyNebulaeProducer.MAX_PER_CELL + 6; i++) { + many.add(cloudAt(0d, 0d, 100d * i, 30d * i * 0.5d)); + } + List sky = SkyNebulaeProducer.around(generatorOf(many), 1L, GalacticCoord.ORIGIN); + + assertTrue("the sky must be capped: " + sky.size(), sky.size() <= SkyNebulaeProducer.MAX_PER_CELL); + for (int i = 1; i < sky.size(); i++) { + assertTrue("clouds must be ordered largest first", + sky.get(i - 1).angularRadius >= sky.get(i).angularRadius); + } + } + + @Test + public void whatIsSeatedAndWhatIsDrawnAreSeparatelyReadable() { + // So a reader can tell a working LOD filter from a missing cloud — the distinction the probe + // reply reports and a test would otherwise have to guess at. + List mixed = Arrays.asList(cloudAt(0d, 0d, 200d, 40d), cloudAt(0d, 0d, 200_000d, 1d)); + IGalaxyGenerator gen = generatorOf(mixed); + + assertEquals("both are out there", 2, SkyNebulaeProducer.countAround(gen, 1L, GalacticCoord.ORIGIN)); + assertEquals("only one is worth drawing", 1, + SkyNebulaeProducer.around(gen, 1L, GalacticCoord.ORIGIN).size()); + } + + @Test + public void aCloudCarriesItsAppearanceAndItsThickness() { + // The two fields the renderer branches on: the age sequence decides the tint, and a dark + // cloud is the one that must be drawn OVER the stars rather than behind them. + Nebula dark = new Nebula(null, Nebula.Appearance.DARK, 0d, 0d, 150d, 40d, 0.6d); + RenderNebula drawn = SkyNebulaeProducer.renderOf(dark, 0d, 0d, 0d); + + assertNotNull(drawn); + assertEquals("the appearance must survive the trip to the client", + Nebula.Appearance.DARK.ordinal(), drawn.appearanceOrdinal); + assertEquals("and so must how thick it is", 0.6F, drawn.opacity, 1.0E-4F); + } +} From 2a9a65b32edd260fc632f64c43309debe2136199 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Sat, 15 Aug 2026 19:57:32 +0300 Subject: [PATCH 21/42] feat: dust between you and a system costs you its detail, not its address - integrate the density along the sight line, once, for both consumers - read the column as magnitudes, cut at the real boundary - an obscured look writes the bare coordinate and says why - zero magnitudes turns the mechanic off entirely --- .../advancedRocketry/api/ARConfiguration.java | 3 + .../command/test/TestProbeCommand.java | 84 ++++++- .../tile/multiblock/TileObservatory.java | 55 ++++- .../universe/ClusteredGalaxyGenerator.java | 18 ++ .../universe/IGalaxyGenerator.java | 13 + .../advancedRocketry/universe/Nebula.java | 25 ++ .../universe/NebulaField.java | 67 ++++++ .../universe/TelescopeScan.java | 72 +++++- .../universe/UniverseRegistry.java | 14 ++ .../assets/advancedrocketry/lang/en_US.lang | 1 + .../assets/advancedrocketry/lang/ru_RU.lang | 1 + .../test/server/NebulaSkyFeedE2ETest.java | 90 +++++++ .../test/unit/NebulaConcealmentTest.java | 223 ++++++++++++++++++ 13 files changed, 653 insertions(+), 13 deletions(-) create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java b/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java index cc96f5e6f..7dc5a1585 100644 --- a/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java +++ b/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java @@ -301,6 +301,8 @@ public class ARConfiguration { @ConfigProperty public int telescopePassiveRadiusCells; @ConfigProperty + public double telescopeObscuredAtMagnitudes; + @ConfigProperty public int telescopeSurveyDataPerStep; @ConfigProperty public boolean allowNonArBiomesInTerraforming; @@ -542,6 +544,7 @@ public static void loadPreInit() { arConfig.telescopeScanTicksPerLightYear = config.get(PLANET, "telescopeScanTicksPerLightYear", 20d, "Extra ticks per light year of distance, per step. This is what makes a far region a longer survey than a near one.", 0d, Double.MAX_VALUE).getDouble(); arConfig.telescopeScanCellsPerStep = config.get(PLANET, "telescopeScanCellsPerStep", 5, "How many cells of the region one step of a survey resolves. This is the bound that stops a sweep from enumerating everything at once.", 1, Integer.MAX_VALUE).getInt(); arConfig.telescopeSurveyDataPerStep = config.get(PLANET, "telescopeSurveyDataPerStep", 0, "Distance data one step of a survey consumes, drawn from the observatory's data buses the same way its asteroid scan draws. A step with too little data waits rather than resolving, so an unfed instrument stalls instead of working for free. Zero (the default) means a survey costs nothing - what it should cost is a balance question, not a mechanic one.", 0, Integer.MAX_VALUE).getInt(); + arConfig.telescopeObscuredAtMagnitudes = config.get(PLANET, "telescopeObscuredAtMagnitudes", 5d, "How much dust a survey can see THROUGH, in magnitudes of visual extinction - the unit astronomy measures interstellar dust in. A nebula between the instrument and what it is looking at dims it; past this much, the survey can still tell that a system is there but can no longer make out its bodies, and writes the bare coordinate instead. The default is the real boundary at which faint objects behind a cloud disappear: ~1 magnitude is noticeable dimming, ~5 is where things start vanishing, ~10 is an opaque dark cloud. Raise it to see through thicker clouds; set it to 0 to turn concealment off entirely.", 0d, Double.MAX_VALUE).getDouble(); arConfig.telescopePassiveRadiusCells = config.get(PLANET, "telescopePassiveRadiusCells", 2, "How far, in CELLS, the passive local radar reaches around the observatory's own cell. Cells and not star territories: this mode watches the neighbourhood, where the planet in the next cell over is a different destination from its star. Passive costs nothing; the directed survey is what looks far away.", 0, Integer.MAX_VALUE).getInt(); DimensionManager.dimOffset = config.getInt("minDimension", PLANET, 2, -127, 8000, "Lowest dimension ID that can be used for planets."); arConfig.canPlayerRespawnInSpace = config.get(PLANET, "allowPlanetRespawn", false, "Allow bed respawn on planets with breathable air.").getBoolean(); diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 71756e223..1cd2fabee 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -3272,17 +3272,88 @@ private void handleSpaceNebulaFind(MinecraftServer server, ICommandSender sender (long) i * stride, 0L, 0L, 0L, 0L, 0L); java.util.List drawn = zmaster587.advancedRocketry.space.SkyNebulaeProducer.around(gen, seed, cell); - if (!drawn.isEmpty()) { - send(sender, "{\"ok\":true,\"found\":true,\"cell\":\"" + cell.cellKey() - + "\",\"sectorX\":" + cell.sectorX() + ",\"drawn\":" + drawn.size() - + ",\"largest\":" + drawn.get(0).angularRadius + ",\"steps\":" + i + "}"); - return; + if (drawn.isEmpty()) { + continue; + } + // WHERE the cloud is, not just that one is visible. A caller measuring a sight line + // THROUGH a cloud needs its centre and its size, and computing them from the render + // record is impossible by design — that record carries a direction and an angle and + // deliberately no position. Taken from the generator's own objects instead. + zmaster587.advancedRocketry.universe.Nebula biggest = null; + for (zmaster587.advancedRocketry.universe.Nebula n : gen.nebulaeAround(seed, cell, + zmaster587.advancedRocketry.space.SkyNebulaeProducer.SKY_REACH_LY)) { + if (biggest == null || n.radiusLy() > biggest.radiusLy()) { + biggest = n; + } } + StringBuilder out = new StringBuilder("{\"ok\":true,\"found\":true,\"cell\":\""); + out.append(cell.cellKey()).append("\",\"sectorX\":").append(cell.sectorX()) + .append(",\"drawn\":").append(drawn.size()) + .append(",\"largest\":").append(drawn.get(0).angularRadius) + .append(",\"steps\":").append(i); + if (biggest != null) { + out.append(",\"centreX\":") + .append(zmaster587.advancedRocketry.universe.UniverseScale + .cellsAt(biggest.centreXLy())) + .append(",\"centreY\":") + .append(zmaster587.advancedRocketry.universe.UniverseScale + .cellsAt(biggest.centreYLy())) + .append(",\"centreZ\":") + .append(zmaster587.advancedRocketry.universe.UniverseScale + .cellsAt(biggest.centreZLy())) + .append(",\"radiusCells\":") + .append(zmaster587.advancedRocketry.universe.UniverseScale + .cellsForLightYears(biggest.radiusLy())) + .append(",\"radiusLy\":").append(biggest.radiusLy()) + .append(",\"peakDensity\":").append(biggest.peakDensity()); + } + out.append('}'); + send(sender, out.toString()); + return; } send(sender, "{\"ok\":true,\"found\":false,\"searched\":" + steps + ",\"stride\":" + stride + "}"); } + /** + * {@code space extinction } — how much the dust between two cells + * dims what is behind it, in magnitudes, plus the raw column it was converted from. + * + *

Both numbers, because they answer different questions: the COLUMN says how much matter the + * line crossed (a fact about the generator) and the MAGNITUDES say what an observer loses (a fact + * about the calibration). A test that saw only one could not tell a generator that seats no + * clouds from a calibration that reads them as transparent.

+ */ + private void handleSpaceExtinction(MinecraftServer server, ICommandSender sender, String[] args) { + zmaster587.advancedRocketry.universe.UniverseRegistry reg = + zmaster587.advancedRocketry.universe.UniverseRegistry.get(server); + if (reg == null) { + send(sender, "{\"error\":\"registry unavailable\"}"); + return; + } + zmaster587.advancedRocketry.space.GalacticCoord from = + zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal( + parseLongOr(args[1], 0L), parseLongOr(args[2], 0L), parseLongOr(args[3], 0L), + 0L, 0L, 0L); + zmaster587.advancedRocketry.space.GalacticCoord to = + zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal( + parseLongOr(args[4], 0L), parseLongOr(args[5], 0L), parseLongOr(args[6], 0L), + 0L, 0L, 0L); + double column = zmaster587.advancedRocketry.universe.UniverseRegistry.getGenerator() + .columnDensityBetween(reg.worldSeed(), from, to); + double magnitudes = reg.extinctionBetween(from, to); + send(sender, "{\"ok\":true,\"from\":\"" + from.cellKey() + "\",\"to\":\"" + to.cellKey() + + "\",\"column\":" + column + ",\"magnitudes\":" + magnitudes + + ",\"obscured\":" + zmaster587.advancedRocketry.universe.TelescopeScan + .isObscured(reg, from, to) + + ",\"threshold\":" + zmaster587.advancedRocketry.api.ARConfiguration + .getCurrentConfig().telescopeObscuredAtMagnitudes + "}"); + } + private void handleSpace(MinecraftServer server, ICommandSender sender, String[] args) { + if (args.length >= 7 && "extinction".equalsIgnoreCase(args[0])) { + handleSpaceExtinction(server, sender, args); + return; + } if (args.length >= 4 && "nebulae".equalsIgnoreCase(args[0])) { handleSpaceNebulae(server, sender, args); return; @@ -11108,6 +11179,9 @@ private void handleMachineTickUntil(MinecraftServer server, ICommandSender sende "telescopeScanCellsPerStep", "telescopePassiveRadiusCells", "telescopeSurveyDataPerStep", + // How much dust a survey sees through, in magnitudes. Flippable at runtime so a + // test can drive BOTH sides of concealment against one generated cloud. + "telescopeObscuredAtMagnitudes", // The research master switch. A survey is instant without it and paced by the // time curve with it, so both halves of boundary B need it flippable at runtime. "planetsMustBeDiscovered")); diff --git a/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java b/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java index 346cf061d..e96db800b 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java @@ -139,6 +139,11 @@ public class TileObservatory extends TileMultiPowerConsumer implements IModularI private RegionScan activeScan; /** How many addresses the last finished scan wrote — what the operator gets told he learned. */ private int lastScanDiscoveries; + /** + * How many of the last scan's looks a cloud stood in the way of. The crystal still gained their + * coordinates; what it did NOT gain is what is at them, and the operator is told which. + */ + private int lastScanObscured; /** Which way the operator has the instrument pointed, as an index into {@link #SCAN_DIRECTIONS}. */ private int scanDirection; /** How far out, in STEPS, he has it aimed. Clamped to the configured reach when it is used. */ @@ -402,6 +407,7 @@ protected void writeNetworkData(NBTTagCompound nbt) { nbt.setTag("regionScan", scan); } nbt.setInteger("lastScanDiscoveries", lastScanDiscoveries); + nbt.setInteger("lastScanObscured", lastScanObscured); nbt.setInteger("scanDirection", scanDirection); nbt.setInteger("scanDistance", scanDistance); nbt.setDouble("scanStepLy", stepLightYears); @@ -425,6 +431,7 @@ protected void readNetworkData(NBTTagCompound nbt) { if (arr != null) for (int v : arr) printedButtonsThisSeed.add(v); activeScan = nbt.hasKey("regionScan") ? RegionScan.readFromNBT(nbt.getCompoundTag("regionScan")) : null; lastScanDiscoveries = nbt.getInteger("lastScanDiscoveries"); + lastScanObscured = nbt.getInteger("lastScanObscured"); scanDirection = nbt.getInteger("scanDirection"); scanDistance = Math.max(1, nbt.getInteger("scanDistance")); stepLightYears = nbt.getDouble("scanStepLy"); @@ -452,6 +459,7 @@ public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt.setTag("regionScan", scan); } nbt.setInteger("lastScanDiscoveries", lastScanDiscoveries); + nbt.setInteger("lastScanObscured", lastScanObscured); nbt.setInteger("scanDirection", scanDirection); nbt.setInteger("scanDistance", scanDistance); nbt.setBoolean("scanPassive", passive); @@ -470,6 +478,7 @@ public void readFromNBT(NBTTagCompound nbt) { activeScan = nbt.hasKey("regionScan") ? RegionScan.readFromNBT(nbt.getCompoundTag("regionScan")) : null; lastScanDiscoveries = nbt.getInteger("lastScanDiscoveries"); + lastScanObscured = nbt.getInteger("lastScanObscured"); scanDirection = nbt.getInteger("scanDirection"); scanDistance = Math.max(1, nbt.getInteger("scanDistance")); passive = nbt.getBoolean("scanPassive"); @@ -797,6 +806,7 @@ public boolean beginRegionScan(int dirX, int dirY, int dirZ, int distanceSteps) world.getTotalWorldTime(), RegionScan.Tuning.fromConfig()); passive = false; lastScanDiscoveries = 0; + lastScanObscured = 0; markDirty(); return true; } @@ -831,6 +841,7 @@ public boolean beginPassiveSweep() { RegionScan.Tuning.fromConfig()); passive = true; lastScanDiscoveries = 0; + lastScanObscured = 0; markDirty(); return true; } @@ -875,12 +886,45 @@ public boolean isPassive() { return passive; } + /** + * How many of the looks in this batch a cloud stands in the way of. + * + *

Counted beside the resolve rather than inside it, because what the OPERATOR is owed and what + * the CRYSTAL is written with are different things: the crystal gains an address either way, and + * the operator needs to know the difference between "there is nothing out that way" and "I cannot + * see through that".

+ */ + private static int countObscured(UniverseRegistry registry, GalacticCoord origin, RegionScan scan, + int from, int count) { + if (registry == null || origin == null || scan == null) { + return 0; + } + int obscured = 0; + for (int index = from; index < from + count && index < scan.totalCells(); index++) { + GalacticCoord cell = scan.cellAt(index); + if (!registry.anchorForCell(cell).isPresent()) { + continue; // empty sky is not a hidden sky + } + if (TelescopeScan.isObscured(registry, origin, registry.anchorForCell(cell).get())) { + obscured++; + } + } + return obscured; + } + /** What the tab tells the operator the instrument is doing right now. */ private String scanStatusText() { if (activeScan != null) { return LibVulpes.proxy.getLocalizedString("msg.observetory.scan.looking") + " " + activeScan.cellsDone() + "/" + activeScan.totalCells(); } + // The dust is reported BEFORE the count of what was found: a survey that came back with + // coordinates and no bodies has a reason, and an operator who is not told it reads the + // instrument as broken. + if (lastScanObscured > 0) { + return LibVulpes.proxy.getLocalizedString("msg.observetory.scan.obscured") + + " " + lastScanObscured; + } if (lastScanDiscoveries > 0) { return LibVulpes.proxy.getLocalizedString("msg.observetory.scan.found") + " " + lastScanDiscoveries; @@ -969,8 +1013,15 @@ private void completeRegionScanIfDue() { extractData(cost, DataType.DISTANCE, EnumFacing.UP, true); } - lastScanDiscoveries += TelescopeScan.resolveBatch(UniverseRegistry.get(world), activeScan, - activeScan.cellsDone(), cells, crystal, now, TelescopeScan.dimensionNames()); + // The look is resolved FROM here, so a cloud standing between this instrument and what it is + // aimed at can cost the look its detail. Counted while we are at it: an operator whose + // survey came back with coordinates and no bodies must be told it was the dust, or the + // feature is indistinguishable from an instrument that found nothing. + GalacticCoord origin = scanOrigin(); + UniverseRegistry registry = UniverseRegistry.get(world); + lastScanObscured += countObscured(registry, origin, activeScan, activeScan.cellsDone(), cells); + lastScanDiscoveries += TelescopeScan.resolveBatch(registry, activeScan, + activeScan.cellsDone(), cells, crystal, now, TelescopeScan.dimensionNames(), origin); activeScan = instant ? activeScan.completed(now) : activeScan.advanced(now, cells); if (activeScan.isComplete()) { activeScan = null; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 4f3413190..80d85a50c 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -584,6 +584,24 @@ public List nebulaeAround(long seed, GalacticCoord cell, double radiusLy supZ - reachSuper, supX + reachSuper, supY + reachSuper, supZ + reachSuper); } + /** + * {@inheritDoc} + * + *

The galaxy is resolved at the OBSERVER's end. Over the ranges a look spans — a survey's + * horizon is ~100 ly against a galaxy thousands across — both ends share one galaxy; a sight line + * that genuinely left one would be looking at another galaxy, which is a different feature.

+ */ + @Override + public double columnDensityBetween(long seed, GalacticCoord from, GalacticCoord to) { + if (from == null || to == null) { + return 0d; + } + GalacticCoord a = from.cellCentre(); + Optional galaxy = galaxies.galaxyOwningSector(seed, a.sectorX(), a.sectorY(), + a.sectorZ()); + return galaxy.isPresent() ? nebulae.columnDensityBetween(seed, galaxy.get(), from, to) : 0d; + } + @Override public Optional anchorAt(long seed, GalacticCoord cell) { Optional g = systemForLattice(seed, diff --git a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java index dccde502b..5c51e4dee 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java @@ -70,6 +70,19 @@ default List nebulaeAround(long seed, GalacticCoord cell, double radiusL return Collections.emptyList(); } + /** + * How much diffuse matter lies between two cells, in density-light-years — the column an + * observer at {@code from} looks THROUGH to see {@code to}. + * + *

The one query every looking-consequence of a cloud is written against, in both directions: + * what a survey loses to a cloud in the way, and what a ship inside one loses looking out, are + * this integral with the endpoints moved. Zero for a generator with no clouds, which is the + * correct answer for clear space and not a stub.

+ */ + default double columnDensityBetween(long seed, GalacticCoord from, GalacticCoord to) { + return 0d; + } + /** * The super-cell edge (in cells) this generator partitions space by — at most one system per * {@code minSpacingCells}-cube. The registry uses it to attribute member cells of AUTHORED systems and diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java b/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java index 9c363756f..5727226bd 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java @@ -39,6 +39,31 @@ public final class Nebula { /** Below this much residual gas a cluster has no cloud left worth drawing. */ static final double MINIMUM_VISIBLE_GAS = 0.05d; + /** + * What one unit of {@link #densityAt} integrated over one light year costs the light behind it, + * in magnitudes of visual extinction ({@code A_V}) — the unit astronomy measures dust in. + * + *

A calibration, not a balance knob. It maps this model's dimensionless density onto a + * physical quantity, so moving it silently redefines every threshold expressed in magnitudes. The + * anchor: a TYPICAL dark cloud should come out at the classic opaque value, {@code A_V ~ 10} — + * the Barnard-object regime, a hole in the star field. These clouds are Gaussian with + * {@code s = radius/2}, so a ray through the centre integrates to {@code peak * s * sqrt(pi)}; + * for a representative dark cloud (radius ~30 ly, peak ~0.6) that is + * {@code 0.6 * 15 * 1.772 ~ 16} density-light-years, giving {@code 10/16 = 0.63}. Rounded to 0.6, + * and the rounding is deliberate: the anchor is itself "a typical cloud" and not a measurement of + * one particular object.

+ * + *

For scale, once converted: {@code A_V ~ 1} is noticeable dimming, {@code ~5} is where faint + * objects behind a cloud disappear, {@code ~10} is opaque in the visible, and a real dense core + * (B68) reaches ~30.

+ */ + public static final double MAGNITUDES_PER_DENSITY_LIGHT_YEAR = 0.6d; + + /** A column of diffuse matter, in density-light-years, read as visual extinction in magnitudes. */ + public static double magnitudesForColumn(double columnDensityLightYears) { + return Math.max(0d, columnDensityLightYears) * MAGNITUDES_PER_DENSITY_LIGHT_YEAR; + } + /** * What a nebula looks like — DERIVED from how much gas is left, never drawn, because the three * appearances are one age sequence and not three options. diff --git a/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java b/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java index aaa863564..0e81fe2b4 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java @@ -4,6 +4,8 @@ import java.util.List; import java.util.Optional; +import zmaster587.advancedRocketry.space.GalacticCoord; + /** * Where the nebulae are — which is: wherever a star cluster still has gas. * @@ -27,6 +29,12 @@ public final class NebulaField { /** How much a cluster's residual gas may vary from the figure its type states. */ private static final double GAS_VARIATION = 0.35d; + /** Step of the column integral, in light years. A cloud is tens across, so its profile is resolved. */ + private static final double COLUMN_SAMPLE_STEP_LY = 1d; + + /** Ceiling on that integral's samples: a bound on WORK, not a statement about the sky. */ + private static final int MAX_COLUMN_SAMPLES = 512; + private final GalaxyGenConfig config; private final ClusterField clusters; @@ -121,6 +129,65 @@ public List nebulaeInRegion(long seed, Galaxy galaxy, long supMinX, long return out; } + /** + * How much diffuse matter lies ALONG A LINE, in density-light-years — the integral of + * {@link #densityAtSector} from one cell to another. + * + *

Built once, on purpose. Every consequence of a cloud that involves LOOKING is this + * number: what a survey loses to a cloud between it and its target, and what a ship inside one + * loses looking out, are the same integral with the endpoints moved. Two functions computing it + * would drift in the third decimal and nobody would notice for months.

+ * + *

Sampled rather than solved. A closed form exists for one Gaussian, but the line crosses an + * arbitrary set of clouds seated on a lattice, and the sampled form stays correct when the + * profile changes. The step is a light year — a cloud is tens of them across, so its profile is + * resolved many times over — and the sample count is bounded, which is a bound on WORK and not a + * physical statement.

+ */ + public double columnDensityBetween(long seed, Galaxy galaxy, GalacticCoord from, + GalacticCoord to) { + if (galaxy == null || from == null || to == null) { + return 0d; + } + GalacticCoord a = from.cellCentre(); + GalacticCoord b = to.cellCentre(); + double ax = UniverseScale.lightYearsForCells(a.sectorX()); + double ay = UniverseScale.lightYearsForCells(a.sectorY()); + double az = UniverseScale.lightYearsForCells(a.sectorZ()); + double bx = UniverseScale.lightYearsForCells(b.sectorX()); + double by = UniverseScale.lightYearsForCells(b.sectorY()); + double bz = UniverseScale.lightYearsForCells(b.sectorZ()); + double dx = bx - ax, dy = by - ay, dz = bz - az; + double lengthLy = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (lengthLy <= 0d) { + return 0d; + } + + int samples = (int) Math.max(2L, Math.min(MAX_COLUMN_SAMPLES, + Math.round(lengthLy / COLUMN_SAMPLE_STEP_LY) + 1L)); + double step = lengthLy / (samples - 1); + double sum = 0d; + for (int i = 0; i < samples; i++) { + double t = i / (double) (samples - 1); + double density = densityAtLightYears(seed, galaxy, ax + dx * t, ay + dy * t, az + dz * t); + // Trapezoid: the endpoints are half-weighted, so the answer does not depend on which + // end the walk started from. + sum += (i == 0 || i == samples - 1) ? density * 0.5d : density; + } + return sum * step; + } + + /** The density at a point stated in light years — what the line integral samples. */ + public double densityAtLightYears(long seed, Galaxy galaxy, double xLy, double yLy, double zLy) { + long s = config.minSpacing; + long sectorX = UniverseScale.cellsAt(xLy); + long sectorY = UniverseScale.cellsAt(yLy); + long sectorZ = UniverseScale.cellsAt(zLy); + Optional nebula = nebulaAt(seed, galaxy, Math.floorDiv(sectorX, s), + Math.floorDiv(sectorY, s), Math.floorDiv(sectorZ, s)); + return nebula.isPresent() ? nebula.get().densityAt(xLy, yLy, zLy) : 0d; + } + /** * How much diffuse matter lies at this cell, {@code 0}..{@code 1} — the one query a consequence * would be written against, whatever the consequence turns out to be. diff --git a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java index 2455a9e01..090eca4c1 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java @@ -5,6 +5,7 @@ import net.minecraft.item.ItemStack; +import zmaster587.advancedRocketry.api.ARConfiguration; import zmaster587.advancedRocketry.api.Constants; import zmaster587.advancedRocketry.dimension.DimensionProperties; import zmaster587.advancedRocketry.item.ItemMemoryCrystal; @@ -47,11 +48,19 @@ public static IntFunction dimensionNames() { */ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int from, int count, ItemStack crystal, long observedTick, IntFunction nameOf) { + return resolveBatch(registry, scan, from, count, crystal, observedTick, nameOf, null); + } + + /** The same, resolved from a stated observer, so a cloud in the way costs the look its detail. */ + public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int from, int count, + ItemStack crystal, long observedTick, IntFunction nameOf, + GalacticCoord observer) { if (!ItemMemoryCrystal.isCrystal(crystal)) { return 0; } CrystalMemory memory = ItemMemoryCrystal.memoryOf(crystal); - int written = resolveBatch(registry, scan, from, count, memory, observedTick, nameOf); + int written = resolveBatch(registry, scan, from, count, memory, observedTick, nameOf, + observer); if (written > 0) { ItemMemoryCrystal.writeMemory(crystal, memory); } @@ -61,16 +70,49 @@ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int f /** The same, onto an already-opened memory. This is where the discovery actually happens. */ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int from, int count, CrystalMemory memory, long observedTick, IntFunction nameOf) { + return resolveBatch(registry, scan, from, count, memory, observedTick, nameOf, null); + } + + /** + * The same, resolved from a stated OBSERVER — the form that can see what is in the way. + * + *

A null observer means "nothing is between us and it", which is what a caller with no + * position can honestly claim, and what every look was before clouds could obscure one.

+ */ + public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int from, int count, + CrystalMemory memory, long observedTick, IntFunction nameOf, + GalacticCoord observer) { if (registry == null || scan == null || memory == null) { return 0; } int written = 0; for (int index = from; index < from + count && index < scan.totalCells(); index++) { - written += resolveCell(registry, scan.cellAt(index), memory, observedTick, nameOf); + written += resolveCell(registry, scan.cellAt(index), memory, observedTick, nameOf, + observer); } return written; } + /** + * Whether a look from {@code observer} to {@code target} is OBSCURED — a cloud between them thick + * enough that a survey can no longer make out what is there, only that something is. + * + *

The threshold is read in magnitudes of extinction, the unit the sky is measured in, and its + * shipped default is the astronomical boundary at which faint objects behind a cloud disappear. + * Zero or less turns the whole mechanic off, which is what "disable the flag" has to mean.

+ */ + public static boolean isObscured(UniverseRegistry registry, GalacticCoord observer, + GalacticCoord target) { + if (registry == null || observer == null || target == null) { + return false; + } + double threshold = ARConfiguration.getCurrentConfig().telescopeObscuredAtMagnitudes; + if (!(threshold > 0d)) { + return false; + } + return registry.extinctionBetween(observer, target) >= threshold; + } + /** * Resolve ONE cell: every body of the system that OWNS it, or the bare coordinate when that * system has no content the registry can name. Void space yields nothing, which is the point of @@ -86,6 +128,22 @@ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int f */ public static int resolveCell(UniverseRegistry registry, GalacticCoord cell, CrystalMemory memory, long observedTick, IntFunction nameOf) { + return resolveCell(registry, cell, memory, observedTick, nameOf, null); + } + + /** + * The same, from a stated OBSERVER, so a cloud in the way can cost the look its detail. + * + *

An obscured look still yields an address. It falls back to the same bare coordinate a + * system with nothing enumerable already produced: the operator learns that something is there + * and has to go and see what. That is the whole mechanic — a reason to FLY somewhere rather than + * survey it from home — and it is why concealment costs detail and never the look itself. A + * survey that quietly returned nothing would be indistinguishable from an empty sky, which is + * the exact defect this instrument was carrying until it was fixed.

+ */ + public static int resolveCell(UniverseRegistry registry, GalacticCoord cell, CrystalMemory memory, + long observedTick, IntFunction nameOf, + GalacticCoord observer) { if (registry == null || cell == null || memory == null) { return 0; } @@ -95,10 +153,12 @@ public static int resolveCell(UniverseRegistry registry, GalacticCoord cell, Cry } int written = 0; boolean namedSomething = false; - for (SystemBody body : registry.systemBodiesAt(anchor.get())) { - namedSomething = true; - if (memory.record(entryFor(body, observedTick, nameOf))) { - written++; + if (!isObscured(registry, observer, anchor.get())) { + for (SystemBody body : registry.systemBodiesAt(anchor.get())) { + namedSomething = true; + if (memory.record(entryFor(body, observedTick, nameOf))) { + written++; + } } } if (!namedSomething) { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java index 6b8f1e4e6..cc06e84e9 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java @@ -821,6 +821,20 @@ public Optional coordForPlanet(int dimId) { return coordForPlanet(DimensionManager.getInstance().getDimensionProperties(dimId)); } + /** + * How much the diffuse matter between two cells dims what is behind it, in magnitudes of + * visual extinction — the unit astronomy states dust in. + * + *

Zero in clear space, and zero for a universe with no clusters. What the number MEANS: + * ~1 is noticeable dimming, ~5 is where faint things behind a cloud disappear, ~10 is opaque in + * the visible. The calibration from this model's density to magnitudes lives on + * {@link Nebula#MAGNITUDES_PER_DENSITY_LIGHT_YEAR} with its anchor written out; what a given + * mechanic does at a given number of magnitudes is that mechanic's own (tunable) business.

+ */ + public double extinctionBetween(GalacticCoord from, GalacticCoord to) { + return Nebula.magnitudesForColumn(generator.columnDensityBetween(worldSeed, from, to)); + } + /** * Whether the system at {@code coord} is known. DERIVED, never stored: a system is known iff any of its * member bodies with a real dimension is in the global known set ({@link DimensionManager#isPlanetKnown}). diff --git a/src/main/resources/assets/advancedrocketry/lang/en_US.lang b/src/main/resources/assets/advancedrocketry/lang/en_US.lang index f0b7052d8..0127d099a 100644 --- a/src/main/resources/assets/advancedrocketry/lang/en_US.lang +++ b/src/main/resources/assets/advancedrocketry/lang/en_US.lang @@ -408,6 +408,7 @@ msg.observetory.scan.region=Observe msg.observetory.scan.region.tooltip=Look at the chosen region and write every system it resolves onto the crystal msg.observetory.scan.looking=Surveyed cells: msg.observetory.scan.found=Addresses written: +msg.observetory.scan.obscured=Dust in the way - coordinates only: msg.observetory.scan.idle=Idle msg.observetory.scan.abort=Stop msg.observetory.scan.abort.tooltip=Stop the survey. Everything already resolved is already on the crystal. diff --git a/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang b/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang index d62deef50..506099b21 100644 --- a/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang +++ b/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang @@ -259,6 +259,7 @@ msg.observetory.scan.region=Наблюдать msg.observetory.scan.region.tooltip=Осмотреть выбранную область и записать в кристалл все системы, которые она разрешит msg.observetory.scan.looking=Осмотрено ячеек: msg.observetory.scan.found=Записано адресов: +msg.observetory.scan.obscured=Мешает пыль — только координаты: msg.observetory.scan.idle=Простаивает msg.observetory.scan.abort=Стоп msg.observetory.scan.abort.tooltip=Прервать обзор. Всё, что уже разрешено, уже лежит в кристалле. diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/NebulaSkyFeedE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/NebulaSkyFeedE2ETest.java index aef84391a..6ca60f184 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/NebulaSkyFeedE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/NebulaSkyFeedE2ETest.java @@ -82,6 +82,96 @@ public void withoutAProceduralGeneratorTheSkyIsEmptyRatherThanInvented() throws assertEquals("and must draw none: " + feed, 0L, field(feed, "drawn")); } + /** The value of a decimal JSON field in a probe reply. */ + private static double decimal(String json, String name) { + String key = "\"" + name + "\":"; + int at = json.indexOf(key); + assertTrue("probe reply has no field " + name + ": " + json, at >= 0); + int from = at + key.length(); + int to = from; + while (to < json.length() && "-+.eE0123456789".indexOf(json.charAt(to)) >= 0) { + to++; + } + return Double.parseDouble(json.substring(from, to)); + } + + @Test + public void aRealCloudDimsWhatIsBehindItAndClearSpaceDoesNot() throws Exception { + // What the unit tier cannot reach: it stubs the column, so it can prove the RULE and never + // that a generated cloud produces a column at all. This walks a real sight line through a + // real cloud in a real world, and a clear line beside it as the control. + String installed = exec(GEN_INSTALL); + assertTrue("the procedural generator must install: " + installed, + installed.contains("\"ok\":true")); + + String found = exec("artest space nebula-find 512 64"); + assertTrue("a dense galaxy must have a cloud somewhere in it: " + found, + found.contains("\"found\":true")); + // A sight line THROUGH the cloud's core: from two radii short of its centre to two radii + // past it, along X. Built from where the generator says the cloud IS — the first version of + // this used the cell the finder was standing in, which was the origin, so the "line" had + // zero length and measured nothing. + long centreX = field(found, "centreX"); + long centreY = field(found, "centreY"); + long centreZ = field(found, "centreZ"); + long radius = field(found, "radiusCells"); + String near = (centreX - 2 * radius) + " " + centreY + " " + centreZ; + String far = (centreX + 2 * radius) + " " + centreY + " " + centreZ; + + String through = exec("artest space extinction " + near + " " + far); + assertTrue("the probe must answer for a real sight line: " + through, + through.contains("\"ok\":true")); + assertTrue("a line that reaches a cloud's neighbourhood must cross SOME matter: " + through, + decimal(through, "column") > 0d); + assertTrue("and the magnitudes must follow the column, not be invented: " + through, + decimal(through, "magnitudes") > 0d); + + // The control: no generator, hence no clusters, hence nothing to cross. + String reset = exec("artest space gen-reset"); + assertTrue("the default generator must be restorable: " + reset, reset.contains("\"ok\":true")); + String clear = exec("artest space extinction " + near + " " + far); + assertEquals("a universe with no clouds must dim nothing: " + clear, 0d, + decimal(clear, "magnitudes"), 1.0E-9d); + } + + @Test + public void theConcealmentThresholdCanBeTurnedOff() throws Exception { + // Driven on the real config: a flag has to REMOVE its mechanic rather than soften it, and + // the reading it is judged against is unchanged either way. + String installed = exec(GEN_INSTALL); + assertTrue("the procedural generator must install: " + installed, + installed.contains("\"ok\":true")); + String found = exec("artest space nebula-find 512 64"); + assertTrue("a dense galaxy must have a cloud somewhere in it: " + found, + found.contains("\"found\":true")); + // A sight line THROUGH the cloud's core: from two radii short of its centre to two radii + // past it, along X. Built from where the generator says the cloud IS — the first version of + // this used the cell the finder was standing in, which was the origin, so the "line" had + // zero length and measured nothing. + long centreX = field(found, "centreX"); + long centreY = field(found, "centreY"); + long centreZ = field(found, "centreZ"); + long radius = field(found, "radiusCells"); + String near = (centreX - 2 * radius) + " " + centreY + " " + centreZ; + String far = (centreX + 2 * radius) + " " + centreY + " " + centreZ; + + try { + exec("artest config set telescopeObscuredAtMagnitudes 0.0001"); + String strict = exec("artest space extinction " + near + " " + far); + assertTrue("at a threshold below the real reading the line must count as obscured: " + + strict, strict.contains("\"obscured\":true")); + + exec("artest config set telescopeObscuredAtMagnitudes 0"); + String off = exec("artest space extinction " + near + " " + far); + assertTrue("with the mechanic off nothing is obscured: " + off, + off.contains("\"obscured\":false")); + assertTrue("and the dust itself is still measured — the flag removes the RULE, not the" + + " physics: " + off, decimal(off, "magnitudes") > 0d); + } finally { + exec("artest config set telescopeObscuredAtMagnitudes 5"); + } + } + @Test public void whatIsSeatedAndWhatIsDrawnAreReportedSeparately() throws Exception { // So a reader can tell a working level-of-detail filter from a missing cloud. Without the two diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java new file mode 100644 index 000000000..92dd424f2 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java @@ -0,0 +1,223 @@ +package zmaster587.advancedRocketry.test.unit; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import org.junit.After; +import org.junit.Test; + +import zmaster587.advancedRocketry.api.Constants; +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.navigation.CrystalMemory; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.EmptyGalaxyGenerator; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.IGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Nebula; +import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.SystemBody; +import zmaster587.advancedRocketry.universe.SystemBodyKind; +import zmaster587.advancedRocketry.universe.TelescopeScan; +import zmaster587.advancedRocketry.universe.UniverseRegistry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * What a cloud between an observer and a system costs the look. + * + *

These pin the player-facing promise and the physics it is stated in: a survey through dust + * still learns that something is THERE (the address), and stops being able to say what (the bodies). + * The mechanic is a reason to fly somewhere rather than survey it from home, so what it may never do + * is make a system vanish — that is indistinguishable from an empty sky, which is the exact defect + * this instrument carried until a survey learned to resolve a look through the system that OWNS the + * cell it looked at.

+ * + *

The THRESHOLD is a tunable and nothing here pins its shipped value; what is pinned is that the + * threshold is read in magnitudes, that it is honoured, and that turning it off restores the clear + * sky exactly.

+ */ +public class NebulaConcealmentTest { + + private static final long STEP = GalaxyGenConfig.DEFAULT_MIN_SPACING; + + /** Where the observer stands, and where the system it is looking at is seated. */ + private static final GalacticCoord HOME = GalacticCoord.ORIGIN; + private static final GalacticCoord TARGET = GalacticCoord.ofSectorLocal(4 * STEP, 0, 0, 0, 0, 0); + + private double previousThreshold; + + private static StellarBody star(int id) { + StellarBody s = new StellarBody(); + s.setId(id); + s.setName("Star-" + id); + return s; + } + + /** + * A generator that reports a stated column of dust between ANY two points, and no systems of its + * own — so what a look loses is decided by the column alone. + */ + private static IGalaxyGenerator dustyBy(final double columnDensityLightYears) { + return new IGalaxyGenerator() { + @Override + public Optional systemAt(long seed, GalacticCoord coord) { + return Optional.empty(); + } + + @Override + public Map systemsInRegion(long seed, GalacticCoord min, + GalacticCoord max) { + return Collections.emptyMap(); + } + + @Override + public double columnDensityBetween(long seed, GalacticCoord from, GalacticCoord to) { + return columnDensityLightYears; + } + }; + } + + /** A registry holding one system with a named planet, seated at {@link #TARGET}. */ + private static UniverseRegistry oneSystem() { + UniverseRegistry.setStarLookup(NebulaConcealmentTest::star); + UniverseRegistry registry = new UniverseRegistry(); + registry.place(TARGET, 4); + registry.addPoi(SystemBody.fixedAt(TARGET, SystemBodyKind.STAR, Constants.INVALID_PLANET, 4)); + registry.addPoi(SystemBody.fixedAt(TARGET, SystemBodyKind.PLANET, 401, 4)); + return registry; + } + + private static int look(UniverseRegistry registry, CrystalMemory crystal) { + return TelescopeScan.resolveCell(registry, TARGET, crystal, 7_000L, + dimId -> "Body-" + dimId, HOME); + } + + /** The column, in density-light-years, that the shipped threshold sits at. */ + private static double columnAtThreshold() { + return zmaster587.advancedRocketry.api.ARConfiguration.getCurrentConfig() + .telescopeObscuredAtMagnitudes / Nebula.MAGNITUDES_PER_DENSITY_LIGHT_YEAR; + } + + @org.junit.Before + public void armThreshold() { + previousThreshold = zmaster587.advancedRocketry.api.ARConfiguration.getCurrentConfig() + .telescopeObscuredAtMagnitudes; + // A stated threshold, so nothing here depends on the shipped default staying put. + zmaster587.advancedRocketry.api.ARConfiguration.getCurrentConfig() + .telescopeObscuredAtMagnitudes = 5d; + } + + @After + public void restoreSeams() { + zmaster587.advancedRocketry.api.ARConfiguration.getCurrentConfig() + .telescopeObscuredAtMagnitudes = previousThreshold; + UniverseRegistry.setGenerator(null); + UniverseRegistry.setStarLookup(null); + } + + @Test + public void aClearSightLineNamesTheBodies() { + // The control. Without it "the dusty case names nothing" would be a statement about a + // fixture that never named anything. + UniverseRegistry.setGenerator(new EmptyGalaxyGenerator()); + UniverseRegistry registry = oneSystem(); + CrystalMemory crystal = new CrystalMemory(); + + look(registry, crystal); + + assertNotNull("a look through clear space must name the system's planet", crystal.forBody(401)); + } + + @Test + public void aLookThroughThickDustLearnsTheADDRESSAndNotTheBODIES() { + // THE mechanic. The operator is left knowing there is something out there and having to go + // and see what — which is the reason to fly rather than survey. + UniverseRegistry.setGenerator(dustyBy(columnAtThreshold() * 2d)); + UniverseRegistry registry = oneSystem(); + CrystalMemory crystal = new CrystalMemory(); + + int written = look(registry, crystal); + + assertTrue("an obscured look must still write something: a system that VANISHES is" + + " indistinguishable from empty sky, which is the defect this whole path had", + written >= 1); + assertEquals("and what it writes is one bare address, not a body list", 1, crystal.size()); + assertTrue("the system's planet must NOT be named through the dust", + crystal.forBody(401) == null); + } + + @Test + public void thinDustDoesNotHideAnything() { + // The other side of the threshold, so "obscured" is a property of how much dust there is and + // not of there being any. + UniverseRegistry.setGenerator(dustyBy(columnAtThreshold() * 0.5d)); + UniverseRegistry registry = oneSystem(); + CrystalMemory crystal = new CrystalMemory(); + + look(registry, crystal); + + assertNotNull("a cloud below the threshold must not cost the look its detail", + crystal.forBody(401)); + } + + @Test + public void theThresholdIsReadInMagnitudes() { + // The unit is the contract: the config states extinction, and the calibration from this + // model's density to magnitudes lives in one place. + UniverseRegistry.setGenerator(dustyBy(columnAtThreshold())); + UniverseRegistry registry = oneSystem(); + + double magnitudes = registry.extinctionBetween(HOME, TARGET); + assertEquals("a column at the threshold must read as the configured magnitudes", + zmaster587.advancedRocketry.api.ARConfiguration.getCurrentConfig() + .telescopeObscuredAtMagnitudes, + magnitudes, 1.0E-6d); + assertTrue("and must be judged obscured at exactly that reading", + TelescopeScan.isObscured(registry, HOME, TARGET)); + } + + @Test + public void turningTheThresholdOffRestoresTheClearSky() { + // A config flag has to REMOVE its mechanic, not soften it. Zero is the off switch, because + // "obscured at zero magnitudes" would otherwise mean everything is always hidden. + UniverseRegistry.setGenerator(dustyBy(columnAtThreshold() * 100d)); + UniverseRegistry registry = oneSystem(); + zmaster587.advancedRocketry.api.ARConfiguration.getCurrentConfig() + .telescopeObscuredAtMagnitudes = 0d; + CrystalMemory crystal = new CrystalMemory(); + + assertFalse("with the mechanic off nothing is obscured, however thick the dust", + TelescopeScan.isObscured(registry, HOME, TARGET)); + look(registry, crystal); + assertNotNull("and the survey names bodies exactly as it did before the feature existed", + crystal.forBody(401)); + } + + @Test + public void aLookWithNoStatedObserverIsNeverObscured() { + // A caller that cannot say where it is standing cannot claim a sight line either. This is + // what keeps every pre-existing call site behaving exactly as it did. + UniverseRegistry.setGenerator(dustyBy(columnAtThreshold() * 100d)); + UniverseRegistry registry = oneSystem(); + CrystalMemory crystal = new CrystalMemory(); + + TelescopeScan.resolveCell(registry, TARGET, crystal, 7_000L, dimId -> "Body-" + dimId); + + assertNotNull("an observer-less look must resolve as it always did", crystal.forBody(401)); + } + + @Test + public void extinctionIsZeroInAUniverseWithNoClouds() { + // The negative leg for the physics itself: no clusters, no gas, no dimming — and no + // fabricated column from a generator that has none. + UniverseRegistry.setGenerator(new EmptyGalaxyGenerator()); + UniverseRegistry registry = oneSystem(); + + assertEquals("clear space dims nothing", 0d, registry.extinctionBetween(HOME, TARGET), + 1.0E-9d); + } +} From 2c56749d918623601cfa815422d7fd7f164ce58c Mon Sep 17 00:00:00 2001 From: StannisMod Date: Sat, 15 Aug 2026 21:49:50 +0300 Subject: [PATCH 22/42] test: the sky cell fixture asks the universe for an empty cell - replace the hardcoded 0/5000/0 with a doubling cell-info search - gate on systemBodies + bodiesAt, the render feed's own predicate - stop before the probe's int-sized sector range - verify each reply echoes the cell that was asked for --- .../BoundarySkyRendersInSlotCellE2ETest.java | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java index 398af76d1..dad782583 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java @@ -117,8 +117,17 @@ public class BoundarySkyRendersInSlotCellE2ETest extends AbstractClientE2ETest { private static final String SKY_CLASS = "zmaster587.advancedRocketry.client.render.planet.BoundarySky"; - /** Cell the ship settles in. sy=5000 dodges the fallback stars (all at sy=sz=0). */ - private static final String CELL = "0 5000 0"; + /** + * Cell the ship settles in — FOUND at run time, never written down. See {@link #findEmptyCell()}. + * + *

It used to be the constant {@code "0 5000 0"}, with the note "dodges the fallback stars". That + * was true while a star's neighbourhood was a few hundred cells wide; once the star lattice became + * metric-true a system owns millions of cells around itself, the constant landed deep inside the + * home system's territory, and the arrangement below ("no body may be synced for the slot yet") + * became false with nothing wrong in production. A cell distance expressed as a bare number expires + * the next time the universe's scale moves — so this one is asked for instead.

+ */ + private String cell; /** * The cell's contents: {@code localX localY localZ kind dimId}. The ship settles at the cell CENTRE, @@ -265,7 +274,8 @@ public void aPilotInASlotCellSeesTheBodiesAndStars() throws Exception { // with it and the pilot is put into it. String setup = exec("artest space entry-setup 1"); assertTrue("entry-setup must install the stack: " + setup, setup.contains("\"ok\":true")); - String settle = exec("artest space ledger-settle " + CELL + " 0"); + cell = findEmptyCell(); + String settle = exec("artest space ledger-settle " + cell + " 0"); assertTrue("ledger-settle must succeed: " + settle, settle.contains("\"ok\":true")); Matcher boundM = BOUND_DIM.matcher(settle); assertTrue("the settle must report which slot the cell was bound to: " + settle, boundM.find()); @@ -308,7 +318,7 @@ public void aPilotInASlotCellSeesTheBodiesAndStars() throws Exception { emptyBefore = capture(slotDim, CELL_CAPTURE_Y, EMPTY_YAW, EMPTY_PITCH, "before_empty"); for (String[] body : SYSTEM) { - String poi = exec("artest space add-poi " + CELL + " " + body[0] + " " + body[1] + " " + String poi = exec("artest space add-poi " + cell + " " + body[0] + " " + body[1] + " " + body[2] + " " + body[3] + " " + body[4] + " 7"); assertTrue("add-poi must register the body: " + poi, poi.contains("\"ok\":true")); } @@ -541,6 +551,49 @@ public void aPilotNearACloudSeesIt() throws Exception { } } + /** + * A cell that belongs to no system, asked of the universe rather than assumed. + * + *

This test supplies the whole contents of its cell itself, so its arrangement needs a cell the + * generator has put NOTHING in — otherwise the "before" captures already hold somebody else's + * planets and every difference below is attributed to the wrong cause. Emptiness is read with the + * feed's own predicate: {@code skyBodiesAt} is the union of the owning system's bodies and the + * cell's own, which {@code cell-info} reports as {@code systemBodies} and {@code bodiesAt}, so both + * must be zero.

+ * + *

The search DOUBLES its distance instead of stepping by a territory, and that is the point: a + * territory's width is a property of the active generator, and the moment this test writes it down + * it inherits an assumption that expires. Doubling reaches past any width there will ever be — it + * only has to stop before {@code Integer.MAX_VALUE}, because the probe parses a sector as an int + * and would SILENTLY answer about cell 0/0/0 for anything wider. Which is why the echoed + * {@code cellKey} is checked against the cell that was asked for.

+ */ + private String findEmptyCell() throws Exception { + StringBuilder tried = new StringBuilder(); + for (long sy = 4096L; sy > 0L && sy <= Integer.MAX_VALUE; sy *= 2L) { + String info = exec("artest space cell-info 0 " + sy + " 0"); + assertTrue("cell-info must answer about the very cell it was asked about, or the sector" + + " overflowed the probe's int parse and it silently answered about the" + + " origin: " + info, + info.contains("\"cellKey\":\"0_" + sy + "_0\"")); + int system = intField(info, "systemBodies"); + int here = intField(info, "bodiesAt"); + tried.append(" 0/").append(sy).append("/0=").append(system).append('+').append(here); + if (system == 0 && here == 0) { + return "0 " + sy + " 0"; + } + } + throw new AssertionError("no cell within the probe's int-sized sector range is free of bodies," + + " so this test has nowhere to arrange its own system; tried (systemBodies+bodiesAt):" + + tried); + } + + private static int intField(String json, String name) { + Matcher m = Pattern.compile("\"" + name + "\":(\\d+)").matcher(json); + assertTrue("cell-info must report " + name + ": " + json, m.find()); + return Integer.parseInt(m.group(1)); + } + /** How many body labels the client's last rendered frame wrote. */ private int labelsDrawn() throws Exception { return skyCounter("labelsDrawnLastFrame"); From c19fcdfaedc057cd0fd78e3f639dab5037d96422 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Sat, 15 Aug 2026 22:20:20 +0300 Subject: [PATCH 23/42] feat: free flight is bounded by acceleration, not by speed - drop the velocity clamp from both Newtonian and both assist laws - rename MAX_SPEED to FA_SETPOINT_MAX_SPEED, bounding the setpoint only - auto-range the HUD velocity bars so they cannot peg - pin the new law: linear gain, first cosmic velocity, assist deceleration --- .../api/FreeFlightPhysics.java | 84 ++++++++++--------- .../client/FreeFlightHudState.java | 36 ++++++-- .../test/unit/FreeFlightAssistsTest.java | 26 ++++-- .../test/unit/FreeFlightPhysicsTest.java | 69 +++++++++++++-- 4 files changed, 158 insertions(+), 57 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/api/FreeFlightPhysics.java b/src/main/java/zmaster587/advancedRocketry/api/FreeFlightPhysics.java index 83870d3a8..6de6549a5 100644 --- a/src/main/java/zmaster587/advancedRocketry/api/FreeFlightPhysics.java +++ b/src/main/java/zmaster587/advancedRocketry/api/FreeFlightPhysics.java @@ -34,11 +34,18 @@ * body-frame velocity setpoint (see {@link #rampSetpoint}); * FA computes the thrust that tracks it, cancelling gravity. Zero * setpoint = hover. - *
  • {@link #step} — Flight Assist OFF: raw Newtonian. Translation - * channels are direct thrust while held; release = coast under - * gravity. The manual brake (Shift) lives here only.
  • + *
  • {@link #step} — Flight Assist OFF: raw Newtonian, and now literally so. + * Translation channels are direct thrust while held; release = coast under + * gravity. The manual brake (Shift) lives here only. There is no ceiling on + * speed — only on acceleration ({@link #MAX_THRUST_ACCEL}), so you go as fast as + * you are willing to burn for and must burn as long again to stop.
  • * * + *

    Where a speed bound is genuinely needed it belongs to the ENVIRONMENT rather than to the + * craft, and it is not imposed here: a vanilla entity's own movement resolves collision against + * the SWEPT box it is about to traverse ({@code Entity.move}), so a rocket cannot pass through + * terrain however fast it goes, and empty space has nothing to hit at all.

    + * *

    Player intent enters via a {@link FreeFlightInput} normalised to [-1, +1]. */ public final class FreeFlightPhysics { @@ -51,17 +58,33 @@ public final class FreeFlightPhysics { public static final double MAX_PITCH_RATE = 4.0; /** Per-tick roll (bank) delta (degrees) at full roll input. */ public static final double MAX_ROLL_RATE = 5.0; - /** Max scalar speed (blocks/tick) — hard cap. */ - public static final double MAX_SPEED = 3.0; + /** + * The ceiling (blocks/tick) on the Flight-Assist velocity SETPOINT — the fastest cruise a pilot + * can dial in with the assist on. + * + *

    It bounds what the assist can be ASKED for, and nothing else. It is not a law of motion and + * not a property of the craft: Flight Assist exists to hold the speed you asked for, so a ceiling + * on the asking is a comfort number and lives here; with the assist OFF there is no speed ceiling + * at all and a craft accelerates for as long as it burns (see {@link #translateNewtonian}).

    + * + *

    This used to be {@code MAX_SPEED}, clamped into BOTH laws, which made the documented + * "raw Newtonian" mode not Newtonian and put a rocket's top speed a factor of ~130 below first + * cosmic velocity — by its own numbers it could not reach orbit. The number itself is unchanged; + * only its reach is.

    + */ + public static final double FA_SETPOINT_MAX_SPEED = 3.0; /** Brake retention factor at full brake (0..1, lower = more aggressive). */ public static final double BRAKE_RETENTION = 0.85; /** Pitch clamp (degrees). */ public static final double PITCH_MAX = 85.0; /** * Arcade ceiling on per-tick thrust acceleration (blocks/tick²). Bounds an - * extremely high thrust-to-weight rocket so motion stays smooth; velocity is - * still bounded independently by {@link #MAX_SPEED}. Normal rockets sit far + * extremely high thrust-to-weight rocket so motion stays smooth. Normal rockets sit far * below this (e.g. TWR 2 → ~0.1), so the cap only bites on absurd builds. + * + *

    This is the only bound on free flight. Nothing caps velocity: a craft that keeps + * burning keeps gaining speed, and how long that takes is the whole cost. At this ceiling a + * turnover crossing of one system is hours rather than the impossibility a speed cap made it.

    */ public static final double MAX_THRUST_ACCEL = 0.5; @@ -90,7 +113,7 @@ public final class FreeFlightPhysics { /** * Per-held-tick change of the velocity setpoint (blocks/tick per tick) at * full channel deflection: holding a key sweeps one axis from 0 to - * {@link #MAX_SPEED} in ~{@code MAX_SPEED/SETPOINT_RAMP} = 60 ticks (3 s). + * {@link #FA_SETPOINT_MAX_SPEED} in ~{@code FA_SETPOINT_MAX_SPEED/SETPOINT_RAMP} = 60 ticks (3 s). */ public static final double SETPOINT_RAMP = 0.05; @@ -409,15 +432,13 @@ public static Step faStep(double mx, double my, double mz, Quat q, cx *= s; cy *= s; cz *= s; } + // No velocity clamp: the ceiling lives on the SETPOINT this law is tracking + // (FA_SETPOINT_MAX_SPEED), so a craft that arrives here faster than the pilot asked for - + // carrying momentum from a Newtonian burn - is decelerated by the thrust budget like + // anything else, instead of having its velocity rewritten under it. double newMx = mx + cx; double newMy = my + cy - gravity; double newMz = mz + cz; - - double speed = Math.sqrt(newMx * newMx + newMy * newMy + newMz * newMz); - if (speed > MAX_SPEED) { - double s = MAX_SPEED / speed; - newMx *= s; newMy *= s; newMz *= s; - } return new Step(newMx, newMy, newMz, e[0], e[1], e[2], thrustApplied); } @@ -453,12 +474,9 @@ public static Step translateNewtonian(double mx, double my, double mz, Quat q, double retain = 1.0 - (1.0 - BRAKE_RETENTION) * brake; newMx *= retain; newMy *= retain; newMz *= retain; } - - double speed = Math.sqrt(newMx * newMx + newMy * newMy + newMz * newMz); - if (speed > MAX_SPEED) { - double s = MAX_SPEED / speed; - newMx *= s; newMy *= s; newMz *= s; - } + // NO speed cap. This law is Newtonian and now says so: thrust while held, coast on release, + // and the only bound is MAX_THRUST_ACCEL. Reaching an absurd speed is the pilot's own affair + // and costs him the time it takes to shed it again. return new Step(newMx, newMy, newMz, e[0], e[1], e[2], thrustApplied); } @@ -614,7 +632,9 @@ public static double[] shipControlAccel(double cx, double cy, double cz, *. Holding a translation key RAMPS the matching axis by * {@link #SETPOINT_RAMP} per tick; releasing leaves the setpoint where it * is; {@code input.cutActive} (X) zeroes the whole vector instantly. The - * result is clamped to {@link #MAX_SPEED} in magnitude. + * result is clamped to {@link #FA_SETPOINT_MAX_SPEED} in magnitude — the one place + * free flight has a speed ceiling, and it bounds what the assist may be asked to hold, + * never what the craft may reach. * * @return new setpoint as {forward, right, up} */ @@ -628,8 +648,8 @@ public static double[] rampSetpoint(double spFwd, double spRight, double spUp, double u = sane(spUp) + input.throttleVertical * SETPOINT_RAMP; double mag = Math.sqrt(f * f + r * r + u * u); - if (mag > MAX_SPEED) { - double s = MAX_SPEED / mag; + if (mag > FA_SETPOINT_MAX_SPEED) { + double s = FA_SETPOINT_MAX_SPEED / mag; f *= s; r *= s; u *= s; } return new double[] {f, r, u}; @@ -686,17 +706,11 @@ public static Step faStep(double mx, double my, double mz, cx *= s; cy *= s; cz *= s; } + // No velocity clamp — see the quaternion faStep: the ceiling is on the setpoint. double newMx = mx + cx; double newMy = my + cy - gravity; double newMz = mz + cz; - // Hard speed cap (always — safety). - double speed = Math.sqrt(newMx * newMx + newMy * newMy + newMz * newMz); - if (speed > MAX_SPEED) { - double s = MAX_SPEED / speed; - newMx *= s; newMy *= s; newMz *= s; - } - return new Step(newMx, newMy, newMz, yawDeg, pitchDeg, rollDeg, thrustApplied); } @@ -774,15 +788,7 @@ public static Step step(double mx, double my, double mz, newMz *= retain; } - // Hard speed cap (always — safety). - double speed = Math.sqrt(newMx * newMx + newMy * newMy + newMz * newMz); - if (speed > MAX_SPEED) { - double s = MAX_SPEED / speed; - newMx *= s; - newMy *= s; - newMz *= s; - } - + // NO speed cap — see translateNewtonian. return new Step(newMx, newMy, newMz, newYaw, newPitch, newRoll, thrustApplied); } diff --git a/src/main/java/zmaster587/advancedRocketry/client/FreeFlightHudState.java b/src/main/java/zmaster587/advancedRocketry/client/FreeFlightHudState.java index 6ed7d0810..80b10a2e7 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/FreeFlightHudState.java +++ b/src/main/java/zmaster587/advancedRocketry/client/FreeFlightHudState.java @@ -38,8 +38,16 @@ public final class FreeFlightHudState { public final double bodyForward, bodyRight, bodyUp; /** Flight-Assist setpoints (body frame, blocks/tick). Valid iff {@link #hasVelocity}. */ public final double faForward, faRight, faUp; - /** Full-scale deflection of the HUD's velocity bars (blocks/tick) - the craft's own top speed, so - * each backend's bars use their whole width instead of a rocket-sized fraction of it. */ + /** + * Full-scale deflection of the HUD's velocity bars (blocks/tick). + * + *

    It is a reference cruise speed, NOT a maximum: free flight has no top speed with the + * assist off, so a bar drawn against a fixed full scale pegs and then tells the pilot nothing for + * the rest of the burn. The scale therefore starts at the craft's cruise reference — so ordinary + * flying looks exactly as it always did — and GROWS to the fastest axis whenever the craft is + * quicker than that. The bars stay a readable picture of the velocity vector's shape at any + * speed; the exact numbers are in the text readout beside them.

    + */ public final double barScale; /** @@ -55,9 +63,14 @@ public final class FreeFlightHudState { /** The coarse jump phase ({@code ShipTransitManager.Phase} ordinal); 0 = not in flight. */ public final int transitPhase; + /** + * @param cruiseReference the speed the bars are scaled against while the craft is no faster than + * it (blocks/tick); above it the scale follows the craft — see + * {@link #barScale} + */ private FreeFlightHudState(int tier, boolean inFlight, boolean flightAssistOn, boolean hasVelocity, double bodyForward, double bodyRight, double bodyUp, - double faForward, double faRight, double faUp, double barScale, + double faForward, double faRight, double faUp, double cruiseReference, int driveState, float driveCharge, int spoolTicks, int transitPhase) { this.driveState = driveState; this.driveCharge = driveCharge; @@ -73,7 +86,20 @@ private FreeFlightHudState(int tier, boolean inFlight, boolean flightAssistOn, b this.faForward = faForward; this.faRight = faRight; this.faUp = faUp; - this.barScale = barScale; + // Grow the scale to whatever the craft is actually doing, per axis and per setpoint, so no + // bar can peg. Both are included because with the assist on the pilot can dial a setpoint the + // craft has not reached yet, and a notch outside the bar is worse than no notch. + double widest = cruiseReference; + if (hasVelocity) { + widest = Math.max(widest, Math.abs(bodyForward)); + widest = Math.max(widest, Math.abs(bodyRight)); + widest = Math.max(widest, Math.abs(bodyUp)); + widest = Math.max(widest, Math.abs(faForward)); + widest = Math.max(widest, Math.abs(faRight)); + widest = Math.max(widest, Math.abs(faUp)); + } + // A NaN velocity (an un-synced backend) must not take the scale to NaN and blank the bars. + this.barScale = (Double.isNaN(widest) || widest <= 0.0) ? cruiseReference : widest; } /** Speed magnitude (blocks/tick) from the body-frame velocity; 0 when velocity is unknown. */ @@ -104,7 +130,7 @@ public static FreeFlightHudState forView(EntityPlayer player, World world) { return new FreeFlightHudState(1, rocket.isInFlight(), rocket.isFlightAssistOn(), true, act[0], act[1], act[2], rocket.getFaSetpointForward(), rocket.getFaSetpointRight(), rocket.getFaSetpointUp(), - FreeFlightPhysics.MAX_SPEED, + FreeFlightPhysics.FA_SETPOINT_MAX_SPEED, 0, 0f, 0, 0); } // The link alone is NOT evidence that a ship exists — it is a build-time intention that diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/FreeFlightAssistsTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/FreeFlightAssistsTest.java index 61b1b3800..a49e493a4 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/FreeFlightAssistsTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/FreeFlightAssistsTest.java @@ -64,16 +64,16 @@ public void cutZeroesTheWholeSetpointInstantly() { public void rampReachesFullScaleInSixtyTicks() { double[] sp = {0, 0, 0}; for (int i = 0; i < 60; i++) sp = FreeFlightPhysics.rampSetpoint(sp[0], sp[1], sp[2], fwd(1f)); - assertEquals(FreeFlightPhysics.MAX_SPEED, sp[0], 1e-9); + assertEquals(FreeFlightPhysics.FA_SETPOINT_MAX_SPEED, sp[0], 1e-9); } @Test - public void setpointMagnitudeIsClampedToMaxSpeed() { + public void setpointMagnitudeIsClampedToTheAssistCeiling() { double[] sp = {0, 0, 0}; FreeFlightInput diag = new FreeFlightInput(1f, 1f, 1f, 0f, 0f, 0f, false); for (int i = 0; i < 300; i++) sp = FreeFlightPhysics.rampSetpoint(sp[0], sp[1], sp[2], diag); double mag = Math.sqrt(sp[0]*sp[0] + sp[1]*sp[1] + sp[2]*sp[2]); - assertEquals(FreeFlightPhysics.MAX_SPEED, mag, 1e-9); + assertEquals(FreeFlightPhysics.FA_SETPOINT_MAX_SPEED, mag, 1e-9); } @Test @@ -187,10 +187,22 @@ public void faStepEchoesOrientationUntouched() { assertEquals(-42f, s.pitch, DELTA); } - @Test - public void faSpeedIsHardCapped() { - Step s = FreeFlightPhysics.faStep(2.9, 0, 0.9, 0f, 0f, 3.0, 3.0, 0, 0.5, 0.0, true); + /** + * Switching the assist ON while flying faster than it can be asked for must not rewrite the + * craft's velocity: FA slows it down with the thrust it has, one budget per tick, like anything + * else. The assist's ceiling binds the SETPOINT (pinned above), never the motion. + * + *

    This is the leg that separates "the ceiling moved onto the setpoint" from "the ceiling is + * still on the velocity, one call later": a clamping build brings 100 blocks/tick back to 3 in a + * single step, which is a stop no engine paid for.

    + */ + @Test + public void faDeceleratesAnOverfastCraftAtItsThrustBudget() { + double entrySpeed = 100.0; + double budget = 0.5; + Step s = FreeFlightPhysics.faStep(0, 0, entrySpeed, 0f, 0f, 0, 0, 0, budget, 0.0, true); double speed = Math.sqrt(s.motionX*s.motionX + s.motionY*s.motionY + s.motionZ*s.motionZ); - assertTrue(speed <= FreeFlightPhysics.MAX_SPEED + DELTA); + assertEquals("FA must shed exactly the thrust budget, not the whole overspeed", + entrySpeed - budget, speed, DELTA); } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/FreeFlightPhysicsTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/FreeFlightPhysicsTest.java index ec1674251..79f8d845b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/FreeFlightPhysicsTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/FreeFlightPhysicsTest.java @@ -28,7 +28,9 @@ * - Climb gate: full vertical climbs iff thrustMag > gravity. * - Yaw/pitch rotate at MAX_*_RATE; pitch clamps to PITCH_MAX. * - canThrust=false → no thrust applied; gravity + rotation still act. - * - Brake attenuates motion; hard speed cap clamps to MAX_SPEED. + * - Brake attenuates motion; NOTHING caps speed — the bound is on acceleration, so + * burning for n ticks buys exactly n x MAX_THRUST_ACCEL and first cosmic velocity + * is reachable. * - Translation is body-relative: forward along the nose, strafe along the * horizontal right axis, vertical along the nose's up axis (tilts with pitch). * - Null input is tolerated (treated as zero). @@ -159,14 +161,69 @@ public void brakeAttenuatesHorizontalMotion() { assertTrue("brake must shrink motionX magnitude", Math.abs(s.motionX) < startX); } + /** + * With the assist off the law bounds ACCELERATION and nothing else: keep burning and you keep + * gaining speed, without limit. + * + *

    The per-tick gain is asserted alongside the total, and that pairing is the test: a build that + * removed the acceleration ceiling too would pass a "goes very fast" assertion, and a build that + * kept a speed cap anywhere would fail the total however small the cap was. The craft coasts + * unaccelerated for the last stretch as a control — a cap would bite there too.

    + */ @Test - public void hardSpeedCapClampsMagnitudeToMaxSpeed() { - Step s = FreeFlightPhysics.step(10, 0, 0, 0f, 0f, FreeFlightInput.zero(), - THRUST, 0.0, true); + public void newtonianFlightBoundsAccelerationAndNotSpeed() { + int burnTicks = 1000; + double previousSpeed = 0.0; + Step s = new Step(0, 0, 0, 0f, 0f, false); + for (int tick = 0; tick < burnTicks; tick++) { + s = FreeFlightPhysics.step(s.motionX, s.motionY, s.motionZ, 0f, 0f, + new FreeFlightInput(1f, 0f, 0f, 0f, 0f), + FreeFlightPhysics.MAX_THRUST_ACCEL, 0.0, true); + double speed = Math.sqrt(s.motionX * s.motionX + + s.motionY * s.motionY + s.motionZ * s.motionZ); + assertTrue("no tick may add more speed than the thrust ceiling; tick " + tick + + " added " + (speed - previousSpeed), + speed - previousSpeed <= FreeFlightPhysics.MAX_THRUST_ACCEL + DELTA); + previousSpeed = speed; + } + double expected = burnTicks * FreeFlightPhysics.MAX_THRUST_ACCEL; + assertEquals("burning for " + burnTicks + " ticks must buy every bit of the speed it paid for", + expected, previousSpeed, DELTA); + + // Control: release the throttle and the craft neither gains nor loses. A surviving cap + // anywhere in the law would show up here as a silent haircut. + Step coast = FreeFlightPhysics.step(s.motionX, s.motionY, s.motionZ, 0f, 0f, + FreeFlightInput.zero(), THRUST, 0.0, true); + double coastSpeed = Math.sqrt(coast.motionX * coast.motionX + + coast.motionY * coast.motionY + coast.motionZ * coast.motionZ); + assertEquals("coasting must preserve the speed exactly", expected, coastSpeed, DELTA); + } + + /** + * The number this law exists for: first cosmic velocity is 7.9 km/s, which in a metre-per-block + * world is 395 blocks/tick. Under the cap this file used to pin (3 blocks/tick) a rocket + * was short of orbital speed by a factor of ~130 — by its own numbers it could not reach orbit. + * + *

    Flown at 0.1 blocks/tick², an ordinary rocket at thrust-to-weight 2, in vacuum.

    + */ + @Test + public void aRocketAtOrdinaryThrustReachesFirstCosmicVelocity() { + double firstCosmicBlocksPerTick = 395.0; + double ordinaryAccel = 0.1; + int ticks = (int) Math.ceil(firstCosmicBlocksPerTick / ordinaryAccel); + + Step s = new Step(0, 0, 0, 0f, 0f, false); + for (int tick = 0; tick < ticks; tick++) { + s = FreeFlightPhysics.step(s.motionX, s.motionY, s.motionZ, 0f, 0f, + new FreeFlightInput(1f, 0f, 0f, 0f, 0f), + ordinaryAccel, 0.0, true); + } double speed = Math.sqrt(s.motionX * s.motionX + s.motionY * s.motionY + s.motionZ * s.motionZ); - assertTrue("hard cap: speed must not exceed MAX_SPEED, got " + speed, - speed <= FreeFlightPhysics.MAX_SPEED + DELTA); + assertTrue("a rocket accelerating at " + ordinaryAccel + " b/t2 must reach first cosmic" + + " velocity (" + firstCosmicBlocksPerTick + " b/t) after " + ticks + + " ticks of burn, got " + speed, + speed >= firstCosmicBlocksPerTick); } @Test From e25f286a0a107d6aa6a23c63d8fbbbcf1fc66c91 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Sun, 16 Aug 2026 18:59:38 +0300 Subject: [PATCH 24/42] feat: the cell grows to 32M, a body carries its size, one generator - raise CELL to 32,000,000 and move the shipyard clear of it - derive the addressing floor a bigger cell spends on inner orbits - size a body by the angle it subtends, from a radius it carries - send the body's radius and its parent on the render channel - delete the legacy random planet generator and derive instead - widen the test JSON readers that could not read an exponent --- .../api/dimension/solar/StellarBody.java | 21 ++ .../client/render/planet/ApparentSize.java | 85 ++++-- .../client/render/planet/BoundarySky.java | 9 +- .../render/planet/RenderAsteroidSky.java | 29 +- .../render/planet/RenderPlanetarySky.java | 29 +- .../command/sub/planet/PlanetCommand.java | 2 +- .../sub/planet/PlanetGenerateCommand.java | 157 +++++------ .../command/test/TestProbeCommand.java | 52 +--- .../dimension/DimensionManager.java | 265 ++---------------- .../network/PacketSystemBodiesSync.java | 51 +++- .../advancedRocketry/space/GalacticCoord.java | 29 +- .../space/SystemBodiesProducer.java | 46 ++- .../universe/ClusteredGalaxyGenerator.java | 80 +++++- .../universe/IGalaxyGenerator.java | 15 + .../universe/PlanetDerivation.java | 17 +- .../advancedRocketry/universe/SystemBody.java | 81 +++++- .../universe/SystemContent.java | 7 +- .../universe/UniverseRegistry.java | 41 ++- .../util/AstronomicalBodyHelper.java | 44 +++ .../assets/advancedrocketry/lang/en_US.lang | 6 +- .../BoundarySkyRendersInSlotCellE2ETest.java | 30 +- .../SpikeFarCoordinateRenderJitterTest.java | 2 +- .../client/VehicleRideClientGroupE2ETest.java | 4 +- .../WorldCommandClientGroupE2ETest.java | 6 +- .../WorldCommandFetchModeratorTest.java | 4 +- .../test/server/BeaconEnableCycleTest.java | 2 +- .../server/InterstellarJumpLegE2ETest.java | 2 +- .../test/server/LowGravFallDamageTest.java | 6 +- .../ParkedShipKeepsItsBodiesE2ETest.java | 42 ++- .../PlanetGenerateMoonNullStarTest.java | 82 ------ ...PilotSeatInASuperheatedAtmosphereTest.java | 2 +- ...TerraformerPoweredCycleOnArPlanetTest.java | 17 +- .../VSCrossingLeavesNoShipBehindE2ETest.java | 2 +- ...SCrossingOutOfAnUnloadedSourceE2ETest.java | 2 +- ...edShipLoadDoesNotKillTheServerE2ETest.java | 2 +- .../test/server/VSShipAutoTakeoffE2ETest.java | 2 +- .../test/server/VSShipCrossingSpikeTest.java | 2 +- .../test/server/VSShipDescentE2ETest.java | 2 +- .../test/server/VSShipEntryE2ETest.java | 2 +- .../test/server/VSUnpilotedEntryE2ETest.java | 2 +- .../test/server/WearAccrualDisableTest.java | 2 +- .../test/server/WearSystemTest.java | 2 +- .../test/server/WeightSystemTest.java | 2 +- ...rldCommandPlanetLifecycleContractTest.java | 6 +- .../test/unit/ApparentSizeTest.java | 92 ++++-- .../unit/ClusteredGalaxyGeneratorTest.java | 84 ++++++ .../test/unit/PacketSystemBodiesSyncTest.java | 12 +- .../test/unit/ShipTransitManagerTest.java | 15 +- .../test/unit/StellarHierarchyTest.java | 60 ++++ .../test/unit/SystemBodyTest.java | 52 ++++ .../test/unit/SystemRetinueTest.java | 11 +- .../chunk_claims/ShipChunkAllocator.java | 18 +- 52 files changed, 1030 insertions(+), 607 deletions(-) delete mode 100644 src/test/java/zmaster587/advancedRocketry/test/server/PlanetGenerateMoonNullStarTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java b/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java index d9b9d385a..7121ea656 100644 --- a/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java +++ b/src/main/java/zmaster587/advancedRocketry/api/dimension/solar/StellarBody.java @@ -86,6 +86,8 @@ public List getSubStars() { * registry's job, because the id space is the registry's; this method only states the * relationship.

    */ + private int maxRetinueBodies; + public void addSubStar(StellarBody star) { if (star.name == null) star.setName(name + "-" + (subStars.size() + 1)); @@ -95,6 +97,25 @@ public void addSubStar(StellarBody star) { star.parentStar = this; } + /** + * How many DERIVED worlds this authored system asks for — the pack's own {@code numPlanets} plus + * {@code numGasGiants}, carried past XML load so the universe layer can honour it. + * + *

    It used to be consumed at world creation by a random generator seeded on the wall clock, so + * two saves of one seed differed and the same defect had to be fixed in two world-making models. + * The number survives; the second model does not. Planets and giants are ONE count here because + * giant-ness is derived from a body's own physics — the procedural model does not take it as an + * instruction.

    + */ + public int getMaxRetinueBodies() { + return maxRetinueBodies; + } + + /** State how many derived worlds this system asks for; negative reads as none. */ + public void setMaxRetinueBodies(int count) { + this.maxRetinueBodies = Math.max(0, count); + } + /** This star's primary, or {@code null} when it is the one its system is named for. */ public StellarBody getParentStar() { return parentStar; diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/ApparentSize.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/ApparentSize.java index 2a919c34c..ffb098585 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/ApparentSize.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/ApparentSize.java @@ -1,52 +1,85 @@ package zmaster587.advancedRocketry.client.render.planet; /** - * How big a fed body is drawn in the cell sky, given how far away it is. + * How big a fed body is drawn in the cell sky, given how far away it is and how big it is. * - *

    The rule is one sentence: strictly decreasing in distance, clamped at both ends. Both - * halves are contract, not polish. The fed range runs from a few thousand blocks (a moon in the - * observer's own cell) to ~109 (the far side of a system's neighbourhood), so an unclamped - * inverse law draws the star at a fraction of a pixel and the near body across the whole sky. And the - * renderer already drops a body whose direction vector is shorter than 10-6, i.e. a body - * vanishes exactly when it is closest — a maximum is what stops that being the only cue.

    + *

    The rule is one sentence: strictly increasing in the body's angular size, clamped at both + * ends. All three parts are contract, not polish.

    * - *

    The mapping is logarithmic because the range spans six decades: a linear one would put every - * body in a system at the minimum and leave the whole scale to be spent inside one cell. Which - * function it is, and the four numbers below, are {@code tunable} — what is contract is that it falls - * with distance and cannot leave {@code [MIN_HALF_SIZE, MAX_HALF_SIZE]}.

    + *

    Why the argument is a RATIO and not a distance. Until 2026-08-16 this took a distance + * alone, so every body at the same range drew the same disc: a moon and a gas giant beside each other + * were indistinguishable, and the only cue a sky gave was "near" versus "far". The honest quantity is + * the angle a body subtends, {@code r/d} — that is what makes a giant outdraw a moon at the same + * range and what makes flying closer grow a world.

    + * + *

    Why the compression stays. The fed range runs from a few thousand blocks (a moon in the + * observer's own cell) to ~109 (the far side of a system's neighbourhood), and radii span + * from a small moon to a star, so an unclamped inverse law draws the star at a fraction of a pixel and + * the near body across the whole sky. The renderer also drops a body whose direction vector is shorter + * than 10-6, i.e. a body vanishes exactly when it is closest — a maximum is what stops that + * being the only cue. So the ratio replaces the distance as the thing being compressed; it does not + * replace the compression. An unclamped angular size is correct and unreadable, and this + * renderer chose readable once, on purpose, with the reason written down.

    + * + *

    The mapping is logarithmic because the range spans many decades. Which function it is, and the + * four numbers below, are {@code tunable} — what is contract is that it RISES with {@code r/d} and + * cannot leave {@code [MIN_HALF_SIZE, MAX_HALF_SIZE]}.

    * *

    Pure arithmetic — no GL, no client state — so the rule can be checked without a client.

    */ public final class ApparentSize { - /** Half-size (in sky units) of a body at or beyond {@link #FAR_BLOCKS}. Never zero. {@code tunable}. */ + /** Half-size (in sky units) of a body at or below {@link #FAR_RATIO}. Never zero. {@code tunable}. */ public static final float MIN_HALF_SIZE = 1.5F; - /** Half-size of a body at or inside {@link #NEAR_BLOCKS}. {@code tunable}. */ + /** Half-size of a body at or above {@link #NEAR_RATIO}. {@code tunable}. */ public static final float MAX_HALF_SIZE = 16.0F; - /** At or below this distance a body is drawn at {@link #MAX_HALF_SIZE}. {@code tunable}. */ - public static final double NEAR_BLOCKS = 2_000d; - /** At or beyond this distance a body is drawn at {@link #MIN_HALF_SIZE}. {@code tunable}. */ - public static final double FAR_BLOCKS = 1.0e9; - private static final double LOG_NEAR = Math.log(NEAR_BLOCKS); - private static final double LOG_SPAN = Math.log(FAR_BLOCKS) - LOG_NEAR; + /** + * The angular size ({@code radius / distance}) at or above which a body is drawn at + * {@link #MAX_HALF_SIZE} — 0.1 rad, about 11 degrees of sky. + * + *

    It replaces a NEAR_BLOCKS of 2 000, which was a distance and therefore meant something + * different for every body: 2 000 blocks is deep inside an Earth (25 512 blocks of radius on the + * shipped chart metric) and a long way outside a small moon. {@code tunable}.

    + */ + public static final double NEAR_RATIO = 0.1d; + /** + * The angular size at or below which a body is drawn at {@link #MIN_HALF_SIZE} — 10-6 + * rad, roughly an Earth seen from a tenth of a light-hour. Below this a body is a point either + * way, and the floor is what keeps it visible at all. {@code tunable}. + */ + public static final double FAR_RATIO = 1.0e-6d; + + private static final double LOG_FAR = Math.log(FAR_RATIO); + private static final double LOG_SPAN = Math.log(NEAR_RATIO) - LOG_FAR; private ApparentSize() { } /** - * The half-size to draw a body at {@code distanceBlocks}. A non-finite or non-positive distance - * is the nearest thing there is, so it takes the maximum rather than becoming invisible. + * The half-size to draw a body of {@code radiusBlocks} seen from {@code distanceBlocks}. + * + *

    A body with no radius of its own ({@code radiusBlocks <= 0} — a belt, a station slot) is not + * a sphere and has no angular size; it takes {@link #MIN_HALF_SIZE}, the marker size, rather than + * being guessed at. A non-finite or non-positive DISTANCE is the nearest thing there is, so it + * takes the maximum rather than becoming invisible.

    */ - public static float halfSizeFor(double distanceBlocks) { - if (Double.isNaN(distanceBlocks) || distanceBlocks <= NEAR_BLOCKS) { + public static float halfSizeFor(double radiusBlocks, double distanceBlocks) { + if (Double.isNaN(radiusBlocks) || radiusBlocks <= 0d) { + return MIN_HALF_SIZE; + } + if (Double.isNaN(distanceBlocks) || distanceBlocks <= 0d) { + return MAX_HALF_SIZE; + } + double ratio = radiusBlocks / distanceBlocks; + if (ratio >= NEAR_RATIO) { return MAX_HALF_SIZE; } - if (distanceBlocks >= FAR_BLOCKS) { + if (ratio <= FAR_RATIO) { return MIN_HALF_SIZE; } - double t = (Math.log(distanceBlocks) - LOG_NEAR) / LOG_SPAN; - return (float) (MAX_HALF_SIZE + (MIN_HALF_SIZE - MAX_HALF_SIZE) * t); + double t = (Math.log(ratio) - LOG_FAR) / LOG_SPAN; + return (float) (MIN_HALF_SIZE + (MAX_HALF_SIZE - MIN_HALF_SIZE) * t); } /** diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/BoundarySky.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/BoundarySky.java index a30af478c..68dd8254c 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/BoundarySky.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/BoundarySky.java @@ -425,10 +425,11 @@ private boolean drawBody(BufferBuilder buffer, PacketSystemBodiesSync.RenderBody float yaw = (float) Math.toDegrees(Math.atan2(nx, nz)); float pitch = (float) Math.toDegrees(Math.asin(Math.max(-1.0F, Math.min(1.0F, ny)))); - // The vector's LENGTH is the true distance to the body at the broadcast tick, so apparent - // size follows it. A fixed size made a moon at 3 km and one at 59 km indistinguishable, and - // left "the planet is crawling away" a thing the sky could not show at all. - float half = ApparentSize.halfSizeFor(len); + // Apparent size follows the ANGLE the body subtends — its own radius over the true distance + // at the broadcast tick. Distance alone made a moon at 3 km and one at 59 km + // indistinguishable; radius alone would not move as a ship approaches. Both, and a giant + // beside a moon finally looks like one. + float half = ApparentSize.halfSizeFor(body.radiusBlocks, len); // The STRICT dimension lookup: the lenient one answers an unknown dimension with the // OVERWORLD's properties, so the star -- which has no dimension of its own -- was drawn diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java index 27c74bd0a..4fa453f95 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderAsteroidSky.java @@ -573,7 +573,7 @@ public void render(float partialTicks, WorldClient world, Minecraft mc) { shadowColorTmp[2] = f3; renderPlanet(buffer, parentProperties, planetOrbitalDistance, multiplier, rotation, false, parentHasRings, - (float) Math.pow(parentProperties.getGravitationalMultiplier(), 0.4), shadowColorTmp, 1); + skyRadiusEarths(parentProperties), shadowColorTmp, 1); xrotangle = 0; GL11.glPopMatrix(); } @@ -599,7 +599,7 @@ public void render(float partialTicks, WorldClient world, Minecraft mc) { shadowColorTmp[2] = f3; renderPlanet(buffer, moons, moons.getParentOrbitalDistance(), multiplier, rotation, moons.hasAtmosphere(), - moons.hasRings, (float) Math.pow(moons.gravitationalMultiplier, 0.4), shadowColorTmp, 1); + moons.hasRings, skyRadiusEarths(moons), shadowColorTmp, 1); GL11.glPopMatrix(); } } @@ -681,8 +681,12 @@ protected EnumFacing getRotationAxis(DimensionProperties properties, BlockPos po return EnumFacing.EAST; } - protected void renderPlanet(BufferBuilder buffer, DimensionProperties properties, float planetOrbitalDistance, float alphaMultiplier, double shadowAngle, boolean hasAtmosphere, boolean hasRing, float gravitationalMultiplier, float[] shadowColorMultiplier, float alphaMultiplier2) { - renderPlanet2(buffer, properties, 20f * AstronomicalBodyHelper.getBodySizeMultiplier(planetOrbitalDistance) * gravitationalMultiplier, alphaMultiplier, shadowAngle, hasRing, shadowColorMultiplier, alphaMultiplier2); + protected void renderPlanet(BufferBuilder buffer, DimensionProperties properties, float separationToObserver, float alphaMultiplier, double shadowAngle, boolean hasAtmosphere, boolean hasRing, float radiusEarths, float[] shadowColorMultiplier, float alphaMultiplier2) { + // Size is the body's OWN radius over the distance to it. It used to be its surface GRAVITY + // over that distance, which drew two worlds of equal size at different sizes and two worlds + // of different size at the same one whenever their densities happened to agree. The scale + // constant is unchanged, so a body of one Earth radius draws exactly as it always did. + renderPlanet2(buffer, properties, 20f * AstronomicalBodyHelper.getBodySizeMultiplier(separationToObserver) * radiusEarths, alphaMultiplier, shadowAngle, hasRing, shadowColorMultiplier, alphaMultiplier2); } protected void renderPlanet2(BufferBuilder buffer, DimensionProperties properties, float size, float alphaMultiplier, double shadowAngle, boolean hasRing, float[] shadowColorMultiplier, float alphaMultiplier2) { @@ -865,4 +869,21 @@ protected void drawStar(BufferBuilder buffer, StellarBody sun, DimensionProperti Tessellator.getInstance().draw(); } } + + /** + * The radius a SKY draws this body at, in Earth radii — its stated radius, or one Earth radius + * when nobody has stated one. + * + *

    The fallback is the same reading the orbital maths already takes ({@code getMoonOrbitalPeriod}: + * a body with no stated bulk is a body assumed to be one Earth radius across), and it is what keeps + * every authored planet looking exactly as it did before bodies had sizes. The alternative — + * treating an unset radius as zero — would make every un-edited world vanish from every sky.

    + */ + protected static float skyRadiusEarths(DimensionProperties properties) { + if (properties == null) { + return 1f; + } + double r = properties.getRadius(); + return r > 0d ? (float) r : 1f; + } } diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java index 458714777..105f4a4b1 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/planet/RenderPlanetarySky.java @@ -904,7 +904,7 @@ else if (afloat != null && (planetPositionTheta < 105 || planetPositionTheta > 2 shadowColorMultiplier = new float[]{shadowColorMultiplier[0] * (1 - multiplier) + f1 * multiplier, shadowColorMultiplier[1] * (1 - multiplier) + f2 * multiplier, shadowColorMultiplier[2] * (1 - multiplier) + f3 * multiplier}; } //System.out.println("draw moon (renderplanet"); - renderPlanet(buffer, parentProperties, planetOrbitalDistance, multiplier, rotation, false, parentHasRings, (float) Math.pow(parentProperties.getGravitationalMultiplier(), 0.4), shadowColorMultiplier, alpha2); + renderPlanet(buffer, parentProperties, planetOrbitalDistance, multiplier, rotation, false, parentHasRings, skyRadiusEarths(parentProperties), shadowColorMultiplier, alpha2); xrotangle = 0; GL11.glPopMatrix(); } @@ -939,7 +939,7 @@ else if (afloat != null && (planetPositionTheta < 105 || planetPositionTheta > 2 shadowColorMultiplier = afloat; shadowColorMultiplier = new float[]{shadowColorMultiplier[0] * (1 - multiplier) + f1 * multiplier, shadowColorMultiplier[1] * (1 - multiplier) + f2 * multiplier, shadowColorMultiplier[2] * (1 - multiplier) + f3 * multiplier}; } - renderPlanet(buffer, moons, moons.getParentOrbitalDistance(), multiplier, rotation, moons.hasAtmosphere(), moons.hasRings, (float) Math.pow(moons.gravitationalMultiplier, 0.4), shadowColorMultiplier, alpha2); + renderPlanet(buffer, moons, moons.getParentOrbitalDistance(), multiplier, rotation, moons.hasAtmosphere(), moons.hasRings, skyRadiusEarths(moons), shadowColorMultiplier, alpha2); GL11.glPopMatrix(); } } @@ -1101,8 +1101,12 @@ protected EnumFacing getRotationAxis(DimensionProperties properties, BlockPos po return EnumFacing.EAST; } - protected void renderPlanet(BufferBuilder buffer, DimensionProperties properties, float planetOrbitalDistance, float alphaMultiplier, double shadowAngle, boolean hasAtmosphere, boolean hasRing, float gravitationalMultiplier, float[] shadowColorMultiplier, float alphaMultiplier2) { - renderPlanet2(buffer, properties, 20f * AstronomicalBodyHelper.getBodySizeMultiplier(planetOrbitalDistance) * gravitationalMultiplier, alphaMultiplier, shadowAngle, hasRing, shadowColorMultiplier, alphaMultiplier2); + protected void renderPlanet(BufferBuilder buffer, DimensionProperties properties, float separationToObserver, float alphaMultiplier, double shadowAngle, boolean hasAtmosphere, boolean hasRing, float radiusEarths, float[] shadowColorMultiplier, float alphaMultiplier2) { + // Size is the body's OWN radius over the distance to it. It used to be its surface GRAVITY + // over that distance, which drew two worlds of equal size at different sizes and two worlds + // of different size at the same one whenever their densities happened to agree. The scale + // constant is unchanged, so a body of one Earth radius draws exactly as it always did. + renderPlanet2(buffer, properties, 20f * AstronomicalBodyHelper.getBodySizeMultiplier(separationToObserver) * radiusEarths, alphaMultiplier, shadowAngle, hasRing, shadowColorMultiplier, alphaMultiplier2); } protected void renderPlanet2(BufferBuilder buffer, DimensionProperties properties, float size, float alphaMultiplier, double shadowAngle, boolean hasRing, float[] shadowColorMultiplier, float alphaMultiplier2) { @@ -1373,4 +1377,21 @@ protected void drawStar(BufferBuilder buffer, StellarBody sun, DimensionProperti Tessellator.getInstance().draw(); } } + + /** + * The radius a SKY draws this body at, in Earth radii — its stated radius, or one Earth radius + * when nobody has stated one. + * + *

    The fallback is the same reading the orbital maths already takes ({@code getMoonOrbitalPeriod}: + * a body with no stated bulk is a body assumed to be one Earth radius across), and it is what keeps + * every authored planet looking exactly as it did before bodies had sizes. The alternative — + * treating an unset radius as zero — would make every un-edited world vanish from every sky.

    + */ + protected static float skyRadiusEarths(DimensionProperties properties) { + if (properties == null) { + return 1f; + } + double r = properties.getRadius(); + return r > 0d ? (float) r : 1f; + } } diff --git a/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetCommand.java b/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetCommand.java index 022ad95ec..2795c6f48 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetCommand.java @@ -8,8 +8,8 @@ public class PlanetCommand extends CommandTreeBase { public PlanetCommand() { addSubcommand(new PlanetResetCommand()); addSubcommand(new PlanetListCommand()); - addSubcommand(new PlanetDeleteCommand()); addSubcommand(new PlanetGenerateCommand()); + addSubcommand(new PlanetDeleteCommand()); addSubcommand(new PlanetSetCommand()); addSubcommand(new PlanetGetCommand()); addSubcommand(new PlanetWeatherCommand()); diff --git a/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java b/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java index 4c17da038..d77dfd883 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java @@ -5,15 +5,35 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.util.math.BlockPos; import net.minecraft.util.text.TextComponentTranslation; +import zmaster587.advancedRocketry.api.Constants; +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; import zmaster587.advancedRocketry.command.sub.ARCommand; import zmaster587.advancedRocketry.dimension.DimensionManager; import zmaster587.advancedRocketry.dimension.DimensionProperties; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.BodyProfile; +import zmaster587.advancedRocketry.universe.PlanetDerivation; import javax.annotation.Nullable; import java.util.Collections; import java.util.List; +/** + * {@code /ar planet generate [moon] } — mint one world in a system. + * + *

    It derives, it does not roll. This command used to front the legacy random generator: + * three "randomness" arguments fed {@code new Random(System.currentTimeMillis())}, so the same command + * on the same world produced a different planet every time and the mod carried two world-making + * models that answered the same question differently. The randomness arguments are gone with the + * model behind them, and the world now comes from the ONE derivation everything else uses + * ({@link PlanetDerivation}), keyed on the star and on how many worlds it already has — so running + * this twice on a fresh world of the same seed gives the same two planets, in the same order.

    + * + *

    What survives unchanged: the name is the operator's, the {@code moon} form parents the new world + * on an existing planet, and exactly one dimension is registered per invocation.

    + */ public class PlanetGenerateCommand extends ARCommand { + @Override public String getName() { return "generate"; @@ -26,102 +46,73 @@ public String getUsage(ICommandSender sender) { @Override public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException { - if (args.length < 1 || args.length > 10) { + if (args.length < 2 || args.length > 3) { throw wrongUsage(sender); } - int starId = parseInt(args[0]); - // Offset beginning after the id - int modOffset = 1; - boolean moon = false; - boolean gas = false; - if (args.length > modOffset && args[modOffset].equalsIgnoreCase("moon")) { - modOffset++; - moon = true; - if (!DimensionManager.getInstance().isDimensionCreated(starId)) { - throw invalidValue("Planet with id", starId); - } - } else if (DimensionManager.getInstance().getStar(starId) == null) { - throw invalidValue("Star with id", starId); + int id = parseInt(args[0]); + int offset = 1; + boolean moon = args.length == 3 && args[offset].equalsIgnoreCase("moon"); + if (moon) { + offset++; } - - if (args.length > modOffset && args[modOffset].equalsIgnoreCase("gas")) { - modOffset++; - gas = true; + if (offset >= args.length) { + throw wrongUsage(sender); } + String name = args[offset]; - // First 3 args are randomness, last 3 args are base value - boolean randArgs = args.length == modOffset + 1 + 3; - boolean fullArgs = args.length == modOffset + 1 + 6; - if (randArgs || fullArgs) { - int planetId = starId; - if (moon) { - starId = DimensionManager.getInstance().getDimensionProperties(planetId).getStarId(); - // The moon branch skips the non-moon star-existence guard (see the - // else-if above), then feeds this re-derived starId to generateRandom - // (which dereferences getStar) and to getStar(...).removePlanet below - // — both NPE if the parent planet's star id resolves to no star. - // Fail with a clean command error instead, mirroring the non-moon guard. - if (DimensionManager.getInstance().getStar(starId) == null) { - throw invalidValue("Star with id", starId); - } + int starId; + DimensionProperties parent = null; + if (moon) { + parent = DimensionManager.getInstance().getDimensionProperties(id); + if (parent == null || !DimensionManager.getInstance().isDimensionCreated(id)) { + throw invalidValue("Planet with id", id); } - DimensionProperties props; - int argsOffset = modOffset; - if (gas) { - if (randArgs) { - // Defaults are from DimensionManager#generateRandomPlanets() - props = DimensionManager.getInstance().generateRandomGasGiant(starId, args[argsOffset++], - 150, 180, 125, - parseInt(args[argsOffset++]), parseInt(args[argsOffset++]), parseInt(args[argsOffset])); - } else { - // Method params are flipped... - String name = args[argsOffset++]; - int atmosphereFactor = parseInt(args[argsOffset++]); - int distanceFactor = parseInt(args[argsOffset++]); - int gravityFactor = parseInt(args[argsOffset++]); - props = DimensionManager.getInstance().generateRandomGasGiant(starId, name, - parseInt(args[argsOffset++]), parseInt(args[argsOffset++]), parseInt(args[argsOffset]), - atmosphereFactor, distanceFactor, gravityFactor); - } - } else { - if (randArgs) { - props = DimensionManager.getInstance().generateRandom(starId, args[argsOffset++], - parseInt(args[argsOffset++]), parseInt(args[argsOffset++]), parseInt(args[argsOffset])); - } else { - // Method params are flipped... - String name = args[argsOffset++]; - int atmosphereFactor = parseInt(args[argsOffset++]); - int distanceFactor = parseInt(args[argsOffset++]); - int gravityFactor = parseInt(args[argsOffset++]); - props = DimensionManager.getInstance().generateRandom(starId, name, - parseInt(args[argsOffset++]), parseInt(args[argsOffset++]), parseInt(args[argsOffset]), - atmosphereFactor, distanceFactor, gravityFactor); - } + starId = parent.getStarId(); + // The parent's star must exist before anything is derived from it: the derivation reads + // the star's own physics, and a planet whose star id resolves to nothing would otherwise + // fail deep inside it rather than here, where the operator can read why. + if (DimensionManager.getInstance().getStar(starId) == null) { + throw invalidValue("Star with id", starId); } - if (props == null) { - throw new CommandException("commands.advancedrocketry.planet.generate.invalid", args[modOffset]); - } else { - sender.sendMessage(new TextComponentTranslation("commands.advancedrocketry.planet.generate.success", args[modOffset])); + } else { + starId = id; + if (DimensionManager.getInstance().getStar(starId) == null) { + throw invalidValue("Star with id", starId); } + } - // If [moon] specified, the generated dim should be a moon orbiting planetId instead of a planet orbiting starId. - if (moon) { - props.setParentPlanet(DimensionManager.getInstance().getDimensionProperties(planetId)); - DimensionManager.getInstance().getStar(starId).removePlanet(props); - } - } else { - throw wrongUsage(sender); + StellarBody star = DimensionManager.getInstance().getStar(starId); + // The index is how many worlds this star already holds, so a second call derives a DIFFERENT + // world rather than the same one again — and the sequence is reproducible on a fresh world. + int index = star.getNumPlanets(); + GalacticCoord anchor = GalacticCoord.ofSectorLocal(starId, 0L, 0L, 0L, 0L, 0L); + int orbit = PlanetDerivation.orbitalDistanceOf(server.getWorld(0).getSeed(), anchor, index, + Math.max(1, index + 1), star); + BodyProfile profile = PlanetDerivation.derive(server.getWorld(0).getSeed(), anchor, anchor, + index, star, moon, orbit); + + int dimId = DimensionManager.getInstance().getNextFreeDim(2); + DimensionProperties props = new DimensionProperties(dimId); + props.setName(name); + props.setStar(star); + props.orbitalDist = orbit; + props.setBulk(profile.massEarths(), profile.radiusEarths()); + props.gravitationalMultiplier = profile.gravityPercent() / 100f; + props.setAtmosphereDensityDirect(profile.pressure()); + props.averageTemperature = profile.temperatureKelvin(); + props.initDefaultAttributes(); + if (moon) { + props.setParentPlanet(parent); } + if (!DimensionManager.getInstance().registerDim(props, true)) { + throw new CommandException("commands.advancedrocketry.planet.generate.invalid", name); + } + sender.sendMessage(new TextComponentTranslation("commands.advancedrocketry.planet.generate.success", name)); } @Override - public List getTabCompletions(MinecraftServer server, ICommandSender sender, String[] args, @Nullable BlockPos targetPos) { - if (args.length == 2) { - return getListOfStringsMatchingLastWord(args, "moon", "gas"); - } - if (args.length == 3) { - return Collections.singletonList("gas"); - } + public List getTabCompletions(MinecraftServer server, ICommandSender sender, String[] args, + @Nullable BlockPos targetPos) { return Collections.emptyList(); } } diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 1cd2fabee..b4331afc1 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -4705,11 +4705,17 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] zmaster587.advancedRocketry.space.GalacticCoord coord = zmaster587.advancedRocketry.space.GalacticCoord.ofSectorLocal(sx, sy, sz, lx, ly, lz); // A POI planted by hand does not move — say so, rather than letting a constructor decide. + // The 11th argument is the body's RADIUS in Earth radii; without it the body has none, + // which is a real state (a belt is not a sphere) and is what the sky draws as a marker. + // A test that wants a body drawn at a size has to say what size, because the renderer + // stopped guessing one from distance. + double poiRadiusEarths = args.length >= 11 ? parseDoubleOr(args[10], 0d) : 0d; zmaster587.advancedRocketry.universe.SystemBody body = - zmaster587.advancedRocketry.universe.SystemBody.fixedAt(coord, kind, dimId, starId); + zmaster587.advancedRocketry.universe.SystemBody.fixedAt(coord, kind, dimId, starId) + .withRadius(poiRadiusEarths); reg.addPoi(body); send(sender, "{\"ok\":true,\"cellKey\":\"" + coord.cellKey() + "\",\"descendTarget\":" - + body.isDescendTarget() + "}"); + + body.isDescendTarget() + ",\"radiusEarths\":" + body.radiusEarths() + "}"); return; } // cell-info [dimId]: what the universe registry says is AT one cell, and by which @@ -5879,48 +5885,6 @@ private void handlePlanet(ICommandSender sender, String[] args) { send(sender, jsonMap(out)); return; } - if (args.length >= 2 && "moon-generate-catch".equalsIgnoreCase(args[0])) { - // /artest planet moon-generate-catch - // - // Repro for C072: run the REAL PlanetGenerateCommand moon path against - // a planet whose star id resolves to no star, and report what it - // throws. Temporarily orphans the planet's star (setStar to an id with - // no StellarBody), invokes execute(...), and restores the original star - // in a finally. Pre-fix the command NPEs (getStar dereferenced inside - // generateRandom); post-fix a star-existence guard on the moon branch - // throws a clean CommandException before any generation. No dimension - // is registered in either case (the throw precedes registerDim), so the - // registered-dim count must be unchanged both pre and post. - int planetDim = parseIntOr(args[1], Integer.MIN_VALUE); - DimensionProperties props = DimensionManager.getInstance().getDimensionProperties(planetDim); - if (props == null) { - send(sender, "{\"error\":\"unknown planet\",\"dim\":" + planetDim + "}"); - return; - } - // Find a star id genuinely absent from the star table. - int bogusStar = 0x40000000; - while (DimensionManager.getInstance().getStar(bogusStar) != null) bogusStar++; - int origStar = props.getStarId(); - int dimsBefore = DimensionManager.getInstance().getRegisteredDimensions().length; - String thrown = "null"; - try { - props.setStar(bogusStar); - new zmaster587.advancedRocketry.command.sub.planet.PlanetGenerateCommand().execute( - sender.getServer(), sender, - new String[]{String.valueOf(planetDim), "moon", "C072Moon", "10", "10", "10"}); - } catch (Throwable t) { - thrown = t.getClass().getSimpleName(); - } finally { - props.setStar(origStar); - } - int dimsAfter = DimensionManager.getInstance().getRegisteredDimensions().length; - send(sender, "{\"ok\":true,\"planetDim\":" + planetDim - + ",\"bogusStar\":" + bogusStar - + ",\"thrown\":\"" + thrown + "\"" - + ",\"dimsBefore\":" + dimsBefore - + ",\"dimsAfter\":" + dimsAfter + "}"); - return; - } send(sender, "{\"error\":\"unknown planet subcommand\"}"); } diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java index 290e631b0..debe237b5 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java @@ -253,163 +253,6 @@ public int getNextFreeStarId() { return -1; } - public DimensionProperties generateRandom(int starId, int atmosphereFactor, int distanceFactor, int gravityFactor) { - return generateRandom(starId, 100, 100, 100, atmosphereFactor, distanceFactor, gravityFactor); - } - - public DimensionProperties generateRandom(int starId, String name, int atmosphereFactor, int distanceFactor, int gravityFactor) { - return generateRandom(starId, name, 100, 100, 100, atmosphereFactor, distanceFactor, gravityFactor); - } - - /** - * Creates and registers a planet with the given properties, Xfactor is the amount of variance from the supplied base property; ie: base - (factor/2) <= generated property value <= base - (factor/2) - * - * @param name name of the planet - * @param baseAtmosphere - * @param baseDistance - * @param baseGravity - * @param atmosphereFactor - * @param distanceFactor - * @param gravityFactor - * @return the new dimension properties created for this planet - */ - public DimensionProperties generateRandom(int starId, String name, int baseAtmosphere, int baseDistance, int baseGravity, int atmosphereFactor, int distanceFactor, int gravityFactor) { - DimensionProperties properties = new DimensionProperties(getNextFreeDim(dimOffset)); - - if (properties.getId() == Constants.INVALID_PLANET) return null; - - if (name.equals("")) properties.setName(getNextName(starId, properties.getId())); - else { - properties.setName(name); - } - properties.setAtmosphereDensityDirect(MathHelper.clamp(baseAtmosphere + random.nextInt(atmosphereFactor) - atmosphereFactor / 2, DimensionProperties.MIN_ATM_PRESSURE, DimensionProperties.MAX_ATM_PRESSURE)); - int newDist = properties.orbitalDist = MathHelper.clamp(baseDistance + random.nextInt(distanceFactor), DimensionProperties.MIN_DISTANCE, DimensionProperties.MAX_DISTANCE); - - properties.gravitationalMultiplier = Math.min(Math.max(0.05f, (baseGravity + random.nextInt(gravityFactor) - gravityFactor / 2f) / 100f), 1.3f); - - double minDistance; - int walkDist = 0; - - do { - minDistance = Double.MAX_VALUE; - - for (IDimensionProperties properties2 : getStar(starId).getPlanets()) { - int dist = Math.abs(((DimensionProperties) properties2).orbitalDist - newDist); - if (minDistance > dist) minDistance = dist; - } - - newDist = properties.orbitalDist + walkDist; - if (walkDist > -1) walkDist = -walkDist - 1; - else walkDist = -walkDist; - - } while (minDistance < 4); - - properties.orbitalDist = newDist; - properties.baseOrbitTheta = random.nextInt(360) * Math.PI / 180d; - - properties.orbitalPhi = (random.nextGaussian() - 0.5d) * 180; - properties.rotationalPhi = (random.nextGaussian() - 0.5d) * 180; - - //Get Star Color - properties.setStar(getStar(starId)); - - //Linear is easier. Earth is nominal! - properties.averageTemperature = AstronomicalBodyHelper.getAverageTemperature(properties.getStar(), properties.getSolarOrbitalDistance(), properties.getAtmosphereDensity()); - - - if (AtmosphereTypes.getAtmosphereTypeFromValue(properties.getAtmosphereDensity()) == AtmosphereTypes.NONE && random.nextInt() % 5 == 0 && !AdvancedRocketryFluids.fluidOxygen.isGaseous()) { - properties.setOceanBlock(AdvancedRocketryBlocks.blockOxygenFluid.getDefaultState()); - properties.setSeaLevel(random.nextInt(6) + 72); - } - - if (random.nextInt() % 10 == 0) { - properties.setSeaLevel(random.nextInt(40) + 43); - } - - properties.skyColor[0] *= 1 - MathHelper.clamp(random.nextFloat() * 0.1f + (70 - (properties.averageTemperature / 3f)) / 100f, 0.2f, 1); - properties.skyColor[1] *= 1 - (random.nextFloat() * .5f); - properties.skyColor[2] *= 1 - MathHelper.clamp(random.nextFloat() * 0.1f + ((properties.averageTemperature / 3f) - 70) / 100f, 0, 1); - - if (random.nextInt() % 50 == 0) { - properties.setHasRings(true); - properties.ringColor[0] = properties.skyColor[0]; - properties.ringColor[1] = properties.skyColor[1]; - properties.ringColor[2] = properties.skyColor[2]; - } - - // A day is DRAWN, log-uniform between a quarter and four times the default. It used to be - // (1/g)^3 * DEFAULT — a fabricated law that made spin a function of surface gravity, which has - // no bearing on it, so a half-gravity world got a day eight times longer than Earth's. - double spinFactor = 0.25d * Math.pow(16d, random.nextDouble()); - properties.rotationalPeriod = (int) Math.max(1L, Math.round(spinFactor - * DimensionProperties.DEFAULT_ROTATIONAL_PERIOD)); - - properties.addBiomes(properties.getViableBiomes(true)); - properties.initDefaultAttributes(); - - registerDim(properties, true); - return properties; - } - - public DimensionProperties generateRandom(int starId, int baseAtmosphere, int baseDistance, int baseGravity, int atmosphereFactor, int distanceFactor, int gravityFactor) { - return generateRandom(starId, "", baseAtmosphere, baseDistance, baseGravity, atmosphereFactor, distanceFactor, gravityFactor); - } - - public DimensionProperties generateRandomGasGiant(int starId, String name, int baseAtmosphere, int baseDistance, int baseGravity, int atmosphereFactor, int distanceFactor, int gravityFactor) { - DimensionProperties properties = new DimensionProperties(getNextFreeDim(dimOffset)); - - if (name.isEmpty()) properties.setName(getNextName(starId, properties.getId())); - else { - properties.setName(name); - } - properties.setAtmosphereDensityDirect(MathHelper.clamp(baseAtmosphere + random.nextInt(atmosphereFactor) - atmosphereFactor / 2, DimensionProperties.MIN_ATM_PRESSURE, DimensionProperties.MAX_ATM_PRESSURE)); - properties.orbitalDist = MathHelper.clamp(baseDistance + random.nextInt(distanceFactor), DimensionProperties.MIN_DISTANCE, 800); - //System.out.println(properties.orbitalDist); - properties.gravitationalMultiplier = Math.min(Math.max(0.05f, (baseGravity + random.nextInt(gravityFactor) - gravityFactor / 2f) / 100f), 1.3f); - - double minDistance; - - do { - minDistance = Double.MAX_VALUE; - - properties.orbitTheta = random.nextInt(360) * (2f * Math.PI) / 360f; - - for (IDimensionProperties properties2 : getStar(starId).getPlanets()) { - double dist = Math.abs(((DimensionProperties) properties2).orbitTheta - properties.orbitTheta); - if (dist < minDistance) minDistance = dist; - } - - } while (minDistance < (Math.PI / 40f)); - - //Get Star Color - properties.setStar(getStar(starId)); - - //Linear is easier. Earth is nominal! - properties.averageTemperature = AstronomicalBodyHelper.getAverageTemperature(properties.getStar(), properties.getSolarOrbitalDistance(), properties.getAtmosphereDensity()); - properties.setGasGiant(true); - - // Rings belong to giants, and on a giant they are the RULE rather than a flourish: all four of - // the Solar System's have them, because only a body that massive has a Roche limit reaching far - // enough past its own surface for a moon to have come apart out there. The rocky-planet path - // still rolls its rare 1-in-50; this is the same story told where it actually happens. - if (random.nextInt(4) != 0) { - properties.setHasRings(true); - properties.ringColor[0] = properties.skyColor[0]; - properties.ringColor[1] = properties.skyColor[1]; - properties.ringColor[2] = properties.skyColor[2]; - } - - // Add all gasses for the default world - for (FluidGasGiantGas gas : AdvancedRocketryFluids.getGasGiantGasses()) { - if (((properties.gravitationalMultiplier * 100) >= gas.getMinGravity()) && (gas.getMaxGravity() >= (properties.gravitationalMultiplier * 100)) && 0 > (Math.random() - gas.getChance())) { - properties.getHarvestableGasses().add(gas.getFluid()); - } - } - - registerDim(properties, true); - return properties; - } - /** * @param dimId dimension id to check * @return true if it can be traveled to, in general if it has a surface @@ -814,81 +657,6 @@ public boolean isPlanetKnown(int dimId) { return knownPlanets != null && knownPlanets.contains(dimId); } - private List generateRandomPlanets(StellarBody star, int numRandomGeneratedPlanets, int numRandomGeneratedGasGiants) { - List dimPropList = new LinkedList<>(); - - Random random = new Random(System.currentTimeMillis()); - - - for (int i = 0; i < numRandomGeneratedGasGiants; i++) { - int baseAtm = 180; - int baseDistance = 100; - - // Atmosphere first, then distance — the order the signature declares. These two arguments - // were swapped, and it was invisible because both quantities sit near 100 while meaning - // entirely different things (see AstronomicalBodyHelper's header: the distance, atmosphere - // and star-temperature scales are three separate 100s). A giant is thick-aired and far - // out; swapped, it came out thin-aired at 180 distance units. - DimensionProperties properties = DimensionManager.getInstance().generateRandomGasGiant(star.getId(), "", baseAtm, baseDistance + 50, 125, 100, 100, 75); - - dimPropList.add(properties); - if (properties.gravitationalMultiplier >= 1f) { - int numMoons = random.nextInt(8); - - for (int ii = 0; ii < numMoons; ii++) { - DimensionProperties moonProperties = DimensionManager.getInstance().generateRandom(star.getId(), properties.getName() + ": " + ii, 25, 100, (int) (properties.gravitationalMultiplier / .02f), 25, 100, 50); - if (moonProperties == null) continue; - - dimPropList.add(moonProperties); - - moonProperties.setParentPlanet(properties); - star.removePlanet(moonProperties); - } - } - } - - for (int i = 0; i < numRandomGeneratedPlanets; i++) { - int baseAtm = 75; - int baseDistance = 100; - - if (i % 4 == 0) { - baseAtm = 0; - } else if (i != 6 && (i + 2) % 4 == 0) baseAtm = 120; - - if (i % 3 == 0) { - baseDistance = 170; - } else if ((i + 1) % 3 == 0) { - baseDistance = 30; - } - - // Atmosphere first, then distance — see the gas-giant call above; the same two arguments - // were swapped here. The tables say what was meant: baseAtm is driven by i % 4 to 0 or 120 - // (an atmosphere table, including the airless world every fourth planet was to be), and - // baseDistance by i % 3 to 170 or 30 (a distance table). - DimensionProperties properties = DimensionManager.getInstance().generateRandom(star.getId(), baseAtm, baseDistance, 125, 100, 100, 75); - - if (properties == null) continue; - - dimPropList.add(properties); - - if (properties.gravitationalMultiplier >= 1f) { - int numMoons = random.nextInt(4); - - for (int ii = 0; ii < numMoons; ii++) { - DimensionProperties moonProperties = DimensionManager.getInstance().generateRandom(star.getId(), properties.getName() + ": " + ii, 25, 100, (int) (properties.gravitationalMultiplier / .02f), 25, 100, 50); - - if (moonProperties == null) continue; - - dimPropList.add(moonProperties); - moonProperties.setParentPlanet(properties); - star.removePlanet(moonProperties); - } - } - } - - return dimPropList; - } - @Nullable private File getCurrentSaveRootDirectory() { File dir = net.minecraftforge.common.DimensionManager.getCurrentSaveRootDirectory(); @@ -971,9 +739,13 @@ public void createAndLoadDimensions(boolean resetFromXml) { } for (StellarBody star : dimCouplingList.stars) { - numRandomGeneratedPlanets = loader.getMaxNumPlanets(star); - numRandomGeneratedGasGiants = loader.getMaxNumGasGiants(star); - dimCouplingList.dims.addAll(generateRandomPlanets(star, numRandomGeneratedPlanets, numRandomGeneratedGasGiants)); + // The pack's body count is CARRIED, not consumed. It used to be spent here by a + // second world-making model seeded on the wall clock, which registered its worlds + // as Forge dimensions up front and made two saves of one seed differ. The count + // now bounds the ONE model's derived retinue for this system, and the worlds are + // realized on arrival like everywhere else. + star.setMaxRetinueBodies(loader.getMaxNumPlanets(star) + + loader.getMaxNumGasGiants(star)); } loadedFromXML = true; @@ -1016,7 +788,8 @@ public void createAndLoadDimensions(boolean resetFromXml) { DimensionManager.getInstance().registerDimNoUpdate(dimensionProperties, !Loader.isModLoaded("GalacticraftCore")); } - generateRandomPlanets(DimensionManager.getInstance().getStar(0), numRandomGeneratedPlanets, numRandomGeneratedGasGiants); + DimensionManager.getInstance().getStar(0) + .setMaxRetinueBodies(numRandomGeneratedPlanets + numRandomGeneratedGasGiants); StellarBody star = new StellarBody(); star.setTemperature(10); @@ -1025,7 +798,7 @@ public void createAndLoadDimensions(boolean resetFromXml) { star.setId(DimensionManager.getInstance().getNextFreeStarId()); star.setName("Wolf 12"); DimensionManager.getInstance().addStar(star); - generateRandomPlanets(star, 5, 0); + star.setMaxRetinueBodies(5); star = new StellarBody(); star.setTemperature(170); @@ -1034,7 +807,7 @@ public void createAndLoadDimensions(boolean resetFromXml) { star.setId(DimensionManager.getInstance().getNextFreeStarId()); star.setName("Epsilon ire"); DimensionManager.getInstance().addStar(star); - generateRandomPlanets(star, 7, 0); + star.setMaxRetinueBodies(7); star = new StellarBody(); star.setTemperature(200); @@ -1043,7 +816,7 @@ public void createAndLoadDimensions(boolean resetFromXml) { star.setId(DimensionManager.getInstance().getNextFreeStarId()); star.setName("Proxima Centaurs"); DimensionManager.getInstance().addStar(star); - generateRandomPlanets(star, 3, 0); + star.setMaxRetinueBodies(3); star = new StellarBody(); star.setTemperature(70); @@ -1052,7 +825,7 @@ public void createAndLoadDimensions(boolean resetFromXml) { star.setId(DimensionManager.getInstance().getNextFreeStarId()); star.setName("Magnis Vulpes"); DimensionManager.getInstance().addStar(star); - generateRandomPlanets(star, 2, 0); + star.setMaxRetinueBodies(2); star = new StellarBody(); @@ -1062,7 +835,7 @@ public void createAndLoadDimensions(boolean resetFromXml) { star.setId(DimensionManager.getInstance().getNextFreeStarId()); star.setName("Ma-Roo"); DimensionManager.getInstance().addStar(star); - generateRandomPlanets(star, 6, 0); + star.setMaxRetinueBodies(6); star = new StellarBody(); star.setTemperature(120); @@ -1071,7 +844,7 @@ public void createAndLoadDimensions(boolean resetFromXml) { star.setId(DimensionManager.getInstance().getNextFreeStarId()); star.setName("Alykitt"); DimensionManager.getInstance().addStar(star); - generateRandomPlanets(star, 3, 1); + star.setMaxRetinueBodies(4); } } @@ -1137,11 +910,11 @@ public void createAndLoadDimensions(boolean resetFromXml) { // duplicate random planets every load. Gate on the true first-run // discriminator: only generate randoms when no persisted dims exist. if (!loadedFromXML && loadedPlanets.isEmpty()) { - //Add planets + // Carry each system's body count into the universe layer instead of spending it on a + // second world-making model here — see the sibling site above. for (StellarBody star : dimCouplingList.stars) { - int numRandomGeneratedPlanets = loader.getMaxNumPlanets(star); - int numRandomGeneratedGasGiants = loader.getMaxNumGasGiants(star); - generateRandomPlanets(star, numRandomGeneratedPlanets, numRandomGeneratedGasGiants); + star.setMaxRetinueBodies(loader.getMaxNumPlanets(star) + + loader.getMaxNumGasGiants(star)); } } diff --git a/src/main/java/zmaster587/advancedRocketry/network/PacketSystemBodiesSync.java b/src/main/java/zmaster587/advancedRocketry/network/PacketSystemBodiesSync.java index ac7ebdf21..8a8a69287 100644 --- a/src/main/java/zmaster587/advancedRocketry/network/PacketSystemBodiesSync.java +++ b/src/main/java/zmaster587/advancedRocketry/network/PacketSystemBodiesSync.java @@ -32,7 +32,8 @@ * {@code writeInt(slotDimId)}, {@code writeInt(bodyCount)} and, per body, * {@code writeInt(kindOrdinal)}, {@code writeLong(localX)}, {@code writeLong(localY)}, * {@code writeLong(localZ)}, {@code writeInt(dimId)}, {@code writeBoolean(descendTarget)}, - * {@code writeLong(boundaryRadius)}; then the NEBULA half, {@code writeInt(dimCount)} and, per dim, + * {@code writeLong(boundaryRadius)}, {@code writeLong(radiusBlocks)}, {@code writeInt(parentIndex)}; + * then the NEBULA half, {@code writeInt(dimCount)} and, per dim, * {@code writeInt(slotDimId)}, {@code writeInt(nebulaCount)} and, per cloud, * {@code writeFloat(dirX/dirY/dirZ)}, {@code writeFloat(angularRadius)}, * {@code writeInt(appearanceOrdinal)}, {@code writeFloat(opacity)}. @@ -49,6 +50,10 @@ public final class PacketSystemBodiesSync extends BasePacket { /** One render body for a slot dim: what to draw and where, plus the descend-target highlight flag. */ public static final class RenderBody { + + /** {@link #parentIndex} of a body that belongs to nothing — a star, a planet, a lone POI. */ + public static final int NO_PARENT = -1; + public final int kindOrdinal; public final long localX; public final long localY; @@ -67,8 +72,39 @@ public static final class RenderBody { */ public final long boundaryRadius; + /** + * How big the body itself is, in blocks — its own radius on the chart metric, not the shell + * around it. + * + *

    Sent because the client cannot derive it: the universe registry is server-side, and a + * procedural world has no dimension to read a radius out of until somebody lands on it. + * Without this the sky sized a body by DISTANCE alone, so a moon and a gas giant side by + * side drew exactly the same disc. Zero for anything that is not a sphere — a belt, a + * station slot — which a renderer must treat as "no size of its own" rather than as + * "infinitely small".

    + */ + public final long radiusBlocks; + + /** + * Index, WITHIN THIS DIM'S BODY LIST, of the body this one belongs to — or {@code -1} for a + * body that belongs to nothing. + * + *

    Structure, which is the half of the feed that was missing: a moon carried a direction + * and a size but no way to say whose moon it was, so the sky could draw a giant and its + * retinue and not tell a pilot they were one destination. An INDEX rather than an id because + * the list is sent as a unit and a procedural body has no id of any kind — it has no + * dimension until somebody lands on it.

    + * + *

    Resolved server-side from the invariant the universe layer already holds: a moon shares + * its parent's CELL, and a cell holds at most one real body with moons excepted. So the + * parent of a moon is the non-moon body of the same cell, and there is never a second + * candidate.

    + */ + public final int parentIndex; + public RenderBody(int kindOrdinal, long localX, long localY, long localZ, int dimId, - boolean descendTarget, long boundaryRadius) { + boolean descendTarget, long boundaryRadius, long radiusBlocks, + int parentIndex) { this.kindOrdinal = kindOrdinal; this.localX = localX; this.localY = localY; @@ -76,12 +112,15 @@ public RenderBody(int kindOrdinal, long localX, long localY, long localZ, int di this.dimId = dimId; this.descendTarget = descendTarget; this.boundaryRadius = boundaryRadius; + this.radiusBlocks = radiusBlocks; + this.parentIndex = parentIndex; } @Override public String toString() { return "RenderBody{kind=" + kindOrdinal + ",dir=" + localX + "," + localY + "," + localZ - + ",dim=" + dimId + ",descend=" + descendTarget + ",shell=" + boundaryRadius + "}"; + + ",dim=" + dimId + ",descend=" + descendTarget + ",shell=" + boundaryRadius + + ",r=" + radiusBlocks + ",parent=" + parentIndex + "}"; } } @@ -206,6 +245,8 @@ public void write(ByteBuf out) { buffer.writeInt(b.dimId); buffer.writeBoolean(b.descendTarget); buffer.writeLong(b.boundaryRadius); + buffer.writeLong(b.radiusBlocks); + buffer.writeInt(b.parentIndex); } } buffer.writeInt(nebulaeByDim.size()); @@ -241,8 +282,10 @@ public void readClient(ByteBuf in) { int dimId = buffer.readInt(); boolean descendTarget = buffer.readBoolean(); long boundaryRadius = buffer.readLong(); + long radiusBlocks = buffer.readLong(); + int parentIndex = buffer.readInt(); bodies.add(new RenderBody(kindOrdinal, localX, localY, localZ, dimId, descendTarget, - boundaryRadius)); + boundaryRadius, radiusBlocks, parentIndex)); } decoded.put(slotDimId, bodies); } diff --git a/src/main/java/zmaster587/advancedRocketry/space/GalacticCoord.java b/src/main/java/zmaster587/advancedRocketry/space/GalacticCoord.java index 40ccbb46d..7636293a3 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/GalacticCoord.java +++ b/src/main/java/zmaster587/advancedRocketry/space/GalacticCoord.java @@ -14,9 +14,8 @@ * *

    The sector grid is the bubble grid: a cell is one {@link #CELL}-block cube, so two * positions with equal sector triples share a cell (and, once loaded, the same world). The local - * offset is kept canonical in {@code [-HALF_CELL, HALF_CELL)}, i.e. within ±2M blocks of the - * cell centre, so every local coordinate stays inside the range where 1.12.2 entity doubles, chunks - * and lighting are crisp. Cell-centre content is at local {@code (0,0,0)}.

    + * offset is kept canonical in {@code [-HALF_CELL, HALF_CELL)}, i.e. within ±16M blocks of the + * cell centre. Cell-centre content is at local {@code (0,0,0)}.

    * *

    The sector triple is a cell NAME, not a place. A cell rides the body it belongs * to, so {@code absolute = sector * CELL + local} is the STATIC-frame reading — true for a void cell @@ -30,8 +29,28 @@ */ public final class GalacticCoord { - /** Edge length of one cell / sector, in blocks. The sector grid is the bubble grid. */ - public static final long CELL = 4_000_000L; + /** + * Edge length of one cell / sector, in blocks. The sector grid is the bubble grid. + * + *

    Why 32M and not the 4M this started at. The old size rested on one sentence — that + * entity doubles, chunks and lighting degrade past ~±2M in 1.12.2 — and all three named + * mechanisms were measured CLEAN out to 24M, on a real player and a real flying ship: walking + * distance, collision stand-off, standing, client/server agreement, camera-step granularity and a + * sub-block position round trip, each against an origin control in the same run. The wall that + * actually existed was a mod constant (the physics mod's reserved shipyard quadrant), and it is + * moved out of the way by {@code ShipChunkAllocator.CHUNK_X_START}, which this size is paired + * with: the two must move together or a pose past the old quadrant is silently cancelled.

    + * + *

    16M of half-cell against 24M measured clean is 1.5× margin. What is NOT covered: + * vanilla documents sound-positioning degradation at 2²⁴ = 16 777 216, which the far + * shell of this cell crosses — accepted knowingly, and cosmetic.

    + * + *

    The size is what lets a system fit inside its own cell at the chart metric the mod already + * ships ({@code AstronomicalBodyHelper.METRES_PER_CHART_BLOCK}): at 250 m/block Jupiter's outer + * moons sit ~7.5M blocks out, which is under half of this half-cell and nearly four times the + * old one.

    + */ + public static final long CELL = 32_000_000L; /** Half a cell; the canonical local offset lives in {@code [-HALF_CELL, HALF_CELL)}. */ public static final long HALF_CELL = CELL / 2L; diff --git a/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java b/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java index 8e770bff9..9cb174320 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java +++ b/src/main/java/zmaster587/advancedRocketry/space/SystemBodiesProducer.java @@ -10,6 +10,7 @@ import zmaster587.advancedRocketry.network.PacketSystemBodiesSync.RenderBody; import zmaster587.advancedRocketry.network.PacketSystemBodiesSync.RenderNebula; import zmaster587.advancedRocketry.universe.SystemBody; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; import zmaster587.advancedRocketry.universe.SystemBodyKind; import zmaster587.advancedRocketry.universe.UniverseRegistry; import zmaster587.libVulpes.network.PacketHandler; @@ -133,11 +134,17 @@ public static Map> buildByDim(Map loa // descendable while carrying a zero shell would draw a boundary of no radius, // which is the unvisited-planet bug above coming back through the other field. long shell = descendable ? DescentShell.radiusAround(b) : 0L; + // The body's own size, converted ONCE here from the universe layer's Earth radii + // into the chart blocks the client draws in. A body with no radius of its own (a + // belt, a station slot) sends zero, which is what says "not a sphere". + long radiusBlocks = Math.round(b.radiusEarths() + * AstronomicalBodyHelper.EARTH_RADIUS_BLOCKS); bodies.add(new RenderBody(b.kind().ordinal(), dir.dx(), dir.dy(), dir.dz(), - renderDimIdOf(b), descendable, shell)); + renderDimIdOf(b), descendable, shell, radiusBlocks, + RenderBody.NO_PARENT)); } } - byDim.put(slotDim, bodies); + byDim.put(slotDim, linkMoonsToTheirParents(found, bodies)); } return byDim; } @@ -282,4 +289,39 @@ public static void reset() { tickCounter = 0; SkyNebulaeProducer.reset(); } + + /** + * Re-emit {@code bodies} with each MOON pointing at the body it belongs to. + * + *

    Resolved from the invariant the universe layer already holds rather than from a new + * identity: a moon shares its parent's CELL, and a cell holds at most one REAL body (moons + * excepted, which is exactly why they can share one). So the parent of a moon is the non-moon + * body of the same cell — and if there is none, the moon says so with {@link + * RenderBody#NO_PARENT} instead of pointing at a neighbour. A wrong parent would draw a moon + * orbiting a world it has nothing to do with, which is worse than an unparented moon.

    + */ + private static List linkMoonsToTheirParents(List source, + List bodies) { + if (source == null || source.size() != bodies.size()) { + return bodies; + } + Map primaryByCell = new LinkedHashMap<>(); + for (int i = 0; i < source.size(); i++) { + SystemBody b = source.get(i); + if (b.kind() != SystemBodyKind.MOON) { + primaryByCell.put(b.name().cellKey(), i); + } + } + List linked = new ArrayList<>(bodies.size()); + for (int i = 0; i < bodies.size(); i++) { + SystemBody b = source.get(i); + RenderBody r = bodies.get(i); + Integer parent = b.kind() == SystemBodyKind.MOON + ? primaryByCell.get(b.name().cellKey()) : null; + linked.add(parent == null ? r + : new RenderBody(r.kindOrdinal, r.localX, r.localY, r.localZ, r.dimId, + r.descendTarget, r.boundaryRadius, r.radiusBlocks, parent)); + } + return linked; + } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 80d85a50c..7186db821 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -87,12 +87,16 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { * Separation band for a companion, in orbital-distance units — 0.01 AU to 2 000 AU, drawn * log-uniformly, which is roughly how real separations are distributed over that range. * - *

    The floor is one cell's worth of orbit, so a companion always gets a cell of its own to be - * addressed by. The ceiling is a quarter of the guaranteed clear space around a system, which is - * what lets that clear space state "no two unrelated stars come this close" without a binary ever - * being mistaken for one.

    + *

    The floor IS one cell's worth of orbit ({@link AstronomicalBodyHelper#MIN_ADDRESSABLE_ORBIT_UNITS}), + * so a companion always gets a cell of its own to be addressed by — derived rather than written + * down, because it was written down as {@code 1} and quietly stopped meaning "one cell" when the + * cell grew, at which point every tightest-band companion landed in the primary's cell, lost the + * seat race and was dropped. The ceiling is a quarter of the guaranteed clear space around a + * system, which is what lets that clear space state "no two unrelated stars come this close" + * without a binary ever being mistaken for one.

    */ - private static final int COMPANION_MIN_SEPARATION = 1; + private static final int COMPANION_MIN_SEPARATION = + AstronomicalBodyHelper.MIN_ADDRESSABLE_ORBIT_UNITS; private static final int COMPANION_MAX_SEPARATION = 200_000; /** * A retinue cannot survive inside a companion's orbit, nor a companion inside the retinue's: a @@ -339,10 +343,31 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { bodies.add(new SystemBody(seat.cell, CellFrame.of(AbsolutePos.ofCellName(cell.cellCentre()), seat.law), BodyEphemeris.STATIC, SystemBodyKind.STAR, Constants.INVALID_PLANET, - companion.getId(), companion.getOrbitalDistance())); + companion.getId(), companion.getOrbitalDistance()) + .withRadius(AstronomicalBodyHelper.starRadiusEarths(companion))); } - int count = retinueSize(seed, cell); + appendRetinue(bodies, seed, cell, star, starId, lattice, taken, outerBound, + retinueSize(seed, cell)); + return bodies; + } + + /** + * Append a system's RETINUE — its worlds, their moons and its belts — to {@code bodies}. + * + *

    Extracted so an AUTHORED system can have one too. The legacy random generator used to fill an + * authored star's system at world creation from {@code new Random(System.currentTimeMillis())}, + * which meant two saves of one seed differed and every fix had to be made twice, in two models + * that answered the same question differently. This is the one model, and an authored system now + * reaches it through {@link #authoredRetinueFor} with the pack's own body count as the bound.

    + * + * @param taken cells already claimed — an authored system passes the cells its authored worlds + * hold, so a derived body can never land on one + * @param count how many major bodies to attempt; the drawn orbits still decide how many FIT + */ + private void appendRetinue(List bodies, long seed, GalacticCoord cell, StellarBody star, + int starId, Lattice lattice, Set taken, double outerBound, + int count) { int outermostOrbit = 0; int innermostGiantOrbit = 0; for (int i = 0; i < count; i++) { @@ -378,8 +403,12 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { // forever while its own moons orbited it, and the identical system authored in XML moved. CellFrame bodyFrame = CellFrame.of(AbsolutePos.ofCellName(cell.cellCentre()), seat.law); // Procedural bodies have no realized dimension yet — a descent (Layer 2) realizes one. + // The body carries its OWN size. Nothing downstream can recover it: a procedural world + // has no dimension until a descent mints one, and the render feed reaches a client with + // no registry to ask. bodies.add(new SystemBody(seat.cell, bodyFrame, BodyEphemeris.STATIC, profile.kind(), - Constants.INVALID_PLANET, starId, orbit)); + Constants.INVALID_PLANET, starId, orbit) + .withRadius(profile.radiusEarths())); outermostOrbit = Math.max(outermostOrbit, orbit); if (profile.kind() == SystemBodyKind.GAS_GIANT && (innermostGiantOrbit == 0 || orbit < innermostGiantOrbit)) { @@ -405,6 +434,34 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { PlanetDerivation.innerOrbit(star) * 2d); addBelt(bodies, seed, cell, (int) Math.min(outerBelt, outerBound), star, lattice, starId, taken, count + 2); + } + + /** + * The retinue an AUTHORED system gets: derived from {@code (seed, anchor)} like every other, and + * bounded by the pack's own {@code numPlanets} rather than by the generator's draw. + * + *

    What this preserves from the generator it replaces: a pack that asks for twelve worlds around + * its star still gets twelve. What it changes, deliberately: the worlds are the same two saves + * running from the same seed, because the clock is no longer an input.

    + * + * @param takenCells the cells the system's AUTHORED bodies already occupy, so nothing derived + * lands on one + */ + public List authoredRetinueFor(long seed, GalacticCoord anchor, StellarBody star, + int starId, int count, Set takenCells) { + List bodies = new ArrayList<>(); + if (star == null || anchor == null || count <= 0) { + return bodies; + } + GalacticCoord cell = anchor.cellCentre(); + Lattice lattice = latticeAt(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ()); + Set taken = new HashSet<>(); + taken.add(cell.cellKey()); + if (takenCells != null) { + taken.addAll(takenCells); + } + appendRetinue(bodies, seed, cell, star, starId, lattice, taken, + maxNamedOrbitUnits(lattice.minEdge()), count); return bodies; } @@ -539,8 +596,13 @@ private void addMoons(List bodies, long seed, GalacticCoord anchor, SystemContent.MOON_UNIT_BLOCKS); // A moon rides its PARENT's frame, so a planet and its moons travel as one destination. // It used to ride a static frame of its own, which pinned the whole family in place. + // A moon's size comes from the SAME derivation a descent will realize it with, so the + // moon a pilot sees from orbit is the moon he lands on. + BodyProfile moonProfile = PlanetDerivation.derive(seed, anchor, parent, j, star, true, + parentOrbit); bodies.add(new SystemBody(parent, parentFrame, law, SystemBodyKind.MOON, - Constants.INVALID_PLANET, starId, parentOrbit)); + Constants.INVALID_PLANET, starId, parentOrbit) + .withRadius(moonProfile.radiusEarths())); } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java index 5c51e4dee..03047807a 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java @@ -88,6 +88,21 @@ default double columnDensityBetween(long seed, GalacticCoord from, GalacticCoord * {@code minSpacingCells}-cube. The registry uses it to attribute member cells of AUTHORED systems and * to bound body-offset clamping ({@code radius <= minSpacingCells/2 - margin}). */ + /** + * The DERIVED retinue an AUTHORED system asks for — {@code count} major bodies from + * {@code (seed, anchor)}, avoiding {@code takenCells}. + * + *

    Default: none. A generator with no procedural content has no retinue to lend, and an + * authored system then holds exactly what its pack authored — which is the honest answer rather + * than a stub, and is what the {@code EmptyGalaxyGenerator} means.

    + */ + default java.util.List authoredRetinueFor(long seed, + zmaster587.advancedRocketry.space.GalacticCoord anchor, + zmaster587.advancedRocketry.api.dimension.solar.StellarBody star, int starId, int count, + java.util.Set takenCells) { + return java.util.Collections.emptyList(); + } + default int minSpacingCells() { return GalaxyGenConfig.DEFAULT_MIN_SPACING; } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java index 920e14d73..262b38c01 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java @@ -205,9 +205,22 @@ public static int orbitalDistanceOf(long seed, GalacticCoord anchor, int index, return (int) clamp(distance, DimensionProperties.MIN_DISTANCE, 1_000_000d); } - /** The innermost orbit this star's system may hold, in Advanced Rocketry distance units. */ + /** + * The innermost orbit this star's system may hold, in Advanced Rocketry distance units. + * + *

    Two floors, and they answer different questions. {@code MIN_DISTANCE} is what the body + * FORMAT can express; {@link AstronomicalBodyHelper#MIN_ADDRESSABLE_ORBIT_UNITS} is what the + * universe can ADDRESS — one cell's worth of orbit, below which a body shares its star's cell and + * is silently dropped in the seat race rather than becoming an ambiguous destination. A dim + * star's zone can sit entirely inside that radius, so without this floor its innermost world is + * generated and then lost, which reads as "the generator drops bodies" and is really "the cell is + * the resolution".

    + */ public static double innerOrbit(StellarBody star) { - return Math.max(DimensionProperties.MIN_DISTANCE, referenceDistance(star) * INNER_ORBIT_FACTOR); + return Math.max( + Math.max(DimensionProperties.MIN_DISTANCE, + AstronomicalBodyHelper.MIN_ADDRESSABLE_ORBIT_UNITS), + referenceDistance(star) * INNER_ORBIT_FACTOR); } /** The outermost orbit this star's system may hold. Always comfortably above {@link #innerOrbit}. */ diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java index a6e033ede..0464b90d1 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java @@ -42,6 +42,14 @@ public final class SystemBody { /** Sentinel for {@link #orbitalDistance()}: this body has no orbit of its own (a star, a POI). */ public static final int ORBIT_UNKNOWN = 0; + /** + * Sentinel for {@link #radiusEarths()}: this body has no radius of its own — a belt, a POI, + * anything that is not a sphere. NOT "we forgot to set one": a consumer draws such a body at its + * minimum size rather than guessing, because guessing is how a moon and a gas giant came to be + * drawn identically. + */ + public static final double RADIUS_UNKNOWN = 0d; + private final GalacticCoord name; private final CellFrame frame; private final BodyEphemeris offsetLaw; @@ -49,6 +57,8 @@ public final class SystemBody { private final int dimId; private final int starId; private final int orbitalDistance; + /** This body's own radius in EARTH radii, or {@link #RADIUS_UNKNOWN}. See {@link #radiusEarths()}. */ + private final double radiusEarths; /** * A body at rest in a STATIC frame — the reading for a POI, a fixture, or anything derived @@ -87,6 +97,12 @@ public SystemBody(GalacticCoord name, CellFrame frame, BodyEphemeris offsetLaw, public SystemBody(GalacticCoord name, CellFrame frame, BodyEphemeris offsetLaw, SystemBodyKind kind, int dimId, int starId, int orbitalDistance) { + this(name, frame, offsetLaw, kind, dimId, starId, orbitalDistance, RADIUS_UNKNOWN); + } + + public SystemBody(GalacticCoord name, CellFrame frame, BodyEphemeris offsetLaw, + SystemBodyKind kind, int dimId, int starId, int orbitalDistance, + double radiusEarths) { if (name == null) { throw new NullPointerException("name"); } @@ -100,6 +116,27 @@ public SystemBody(GalacticCoord name, CellFrame frame, BodyEphemeris offsetLaw, this.dimId = dimId; this.starId = starId; this.orbitalDistance = orbitalDistance; + this.radiusEarths = Double.isNaN(radiusEarths) || radiusEarths < 0d + ? RADIUS_UNKNOWN : radiusEarths; + } + + /** + * How big this body actually is, in EARTH radii, or {@link #RADIUS_UNKNOWN}. + * + *

    A body's size is a property of the body, and it travels with it because nothing downstream + * can recover it: a procedural world has no dimension to look it up in until somebody lands on + * it, and the render feed reaches a client that cannot see the universe registry at all. Until + * this existed the sky sized every body by DISTANCE alone, so a moon and a gas giant beside each + * other drew identically.

    + */ + public double radiusEarths() { + return radiusEarths; + } + + /** The same body, carrying {@code radiusEarths}. The generators' way of stating a body's size. */ + public SystemBody withRadius(double newRadiusEarths) { + return new SystemBody(name, frame, offsetLaw, kind, dimId, starId, orbitalDistance, + newRadiusEarths); } private static GalacticCoord requireAddress(GalacticCoord address) { @@ -220,7 +257,8 @@ public boolean definesFrame() { public SystemBody withFrame(CellFrame newFrame) { return newFrame == null || newFrame.equals(frame) ? this - : new SystemBody(name, newFrame, offsetLaw, kind, dimId, starId, orbitalDistance); + : new SystemBody(name, newFrame, offsetLaw, kind, dimId, starId, orbitalDistance, + radiusEarths); } public void writeToNBT(NBTTagCompound nbt) { @@ -233,6 +271,9 @@ public void writeToNBT(NBTTagCompound nbt) { if (orbitalDistance != ORBIT_UNKNOWN) { nbt.setInteger("orbitalDist", orbitalDistance); } + if (radiusEarths != RADIUS_UNKNOWN) { + nbt.setDouble("radiusEarths", radiusEarths); + } } public static SystemBody readFromNBT(NBTTagCompound nbt) { @@ -247,7 +288,8 @@ public static SystemBody readFromNBT(NBTTagCompound nbt) { kind, nbt.hasKey("dimId") ? nbt.getInteger("dimId") : Constants.INVALID_PLANET, nbt.getInteger("starId"), - nbt.getInteger("orbitalDist")); + nbt.getInteger("orbitalDist"), + nbt.getDouble("radiusEarths")); } @Override @@ -261,6 +303,7 @@ public boolean equals(Object o) { SystemBody other = (SystemBody) o; return dimId == other.dimId && starId == other.starId && kind == other.kind && orbitalDistance == other.orbitalDistance + && Double.compare(radiusEarths, other.radiusEarths) == 0 && name.equals(other.name) && offsetLaw.equals(other.offsetLaw) && frame.equals(other.frame); } @@ -272,6 +315,7 @@ public int hashCode() { result = 31 * result + dimId; result = 31 * result + starId; result = 31 * result + orbitalDistance; + result = 31 * result + Double.hashCode(radiusEarths); result = 31 * result + offsetLaw.hashCode(); return result; } @@ -282,10 +326,41 @@ public String toString() { + (offsetLaw.isStatic() ? "" : " +orbit") + "]"; } + /** + * An in-cell offset held inside the cell, reporting the first time it has to. + * + *

    The clamp itself is right: a body outside its own neighbourhood would be a body in a + * different cell, so saturating is the only safe answer. What was wrong is that it was SILENT. + * An orbit that overflows does not fail — every point of it beyond the face collapses onto the + * face, so a giant's outer moons stack at one spot and stop moving, which is a defect that gets + * looked for in the renderer, in the ephemeris and in the frame before anyone suspects a clamp. + * One line per axis per JVM run, naming the overflow, turns a week into a grep.

    + */ private static long clampInCell(long v) { if (v > MAX_IN_CELL) { + reportOverflow(v, MAX_IN_CELL); return MAX_IN_CELL; } - return v < -GalacticCoord.HALF_CELL ? -GalacticCoord.HALF_CELL : v; + if (v < -GalacticCoord.HALF_CELL) { + reportOverflow(v, -GalacticCoord.HALF_CELL); + return -GalacticCoord.HALF_CELL; + } + return v; } + + /** Said ONCE per distinct overflow magnitude: a flooded log is a log nobody reads either. */ + private static void reportOverflow(long raw, long clamped) { + if (REPORTED_OVERFLOWS.add(raw / GalacticCoord.CELL)) { + LOGGER.error("a body's in-cell offset {} is outside its own cell (half-cell {}) and was " + + "flattened onto the face at {}. Every further point of that orbit lands " + + "on the same spot, so the body will appear to stop moving: its orbit is " + + "wider than the cell that names it.", + raw, GalacticCoord.HALF_CELL, clamped); + } + } + + private static final org.apache.logging.log4j.Logger LOGGER = + org.apache.logging.log4j.LogManager.getLogger("advancedrocketry/universe"); + private static final java.util.Set REPORTED_OVERFLOWS = + java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap()); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java index 879f8326c..b9866d9ad 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java @@ -144,7 +144,8 @@ public static List bodiesOf(StellarBody star, GalacticCoord systemCo AbsolutePos anchorAbs = AbsolutePos.ofCellName(anchor); // The star sits at the anchor and does not move: a degenerate frame, not an exemption. bodies.add(new SystemBody(anchor, CellFrame.staticAt(anchor), BodyEphemeris.STATIC, - SystemBodyKind.STAR, Constants.INVALID_PLANET, starId)); + SystemBodyKind.STAR, Constants.INVALID_PLANET, starId, SystemBody.ORBIT_UNKNOWN, + AstronomicalBodyHelper.starRadiusEarths(star))); for (IDimensionProperties p : star.getPlanets()) { if (!(p instanceof DimensionProperties)) { @@ -159,7 +160,7 @@ public static List bodiesOf(StellarBody star, GalacticCoord systemCo // only when it was procedural would be a field that lies for half the galaxy. bodies.add(new SystemBody(planetName, planetFrame, BodyEphemeris.STATIC, kindOf(planet, SystemBodyKind.PLANET), planet.getId(), starId, - planet.getOrbitalDist())); + planet.getOrbitalDist(), planet.getRadius())); for (int moonId : planet.getChildPlanets()) { DimensionProperties moon = DimensionManager.getInstance().getDimensionProperties(moonId); @@ -173,7 +174,7 @@ public static List bodiesOf(StellarBody star, GalacticCoord systemCo // positions it. Same convention as the procedural side. bodies.add(new SystemBody(planetName, planetFrame, moonLawOf(moon, planet), kindOf(moon, SystemBodyKind.MOON), moon.getId(), starId, - planet.getOrbitalDist())); + planet.getOrbitalDist(), moon.getRadius())); } } auditOneRealBodyPerCell(bodies, starId); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java index cc06e84e9..3389d13fa 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java @@ -4,6 +4,8 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; import java.util.List; import java.util.Map; import java.util.Optional; @@ -461,10 +463,12 @@ private List allSystemBodies(GalacticCoord anchor) { Integer id = byCell.get(key); if (id != null) { StellarBody star = starLookup.apply(id); - return star == null - ? new ArrayList() - : SystemContent.bodiesOf(star, anchor, generator.minSpacingCells(), - this::durableName); + if (star == null) { + return new ArrayList(); + } + List authored = SystemContent.bodiesOf(star, anchor, + generator.minSpacingCells(), this::durableName); + return withDerivedRetinue(anchor, star, id, authored); } return new ArrayList<>(generator.bodiesFor(worldSeed, anchor)); } @@ -1210,4 +1214,33 @@ StellarBody toStar() { return star; } } + + /** + * An authored system's bodies, plus the DERIVED worlds its pack asked for. + * + *

    An authored system used to be filled by a second world-making model: a random generator seeded + * on {@code System.currentTimeMillis()} that registered Forge dimensions up front at world + * creation. It meant two saves of one seed differed, and every defect in this family had to be + * found and fixed twice in two models that answered the same question differently. The pack-facing + * knob survives as {@link StellarBody#getMaxRetinueBodies()}; the second model does not, and the + * worlds it used to mint are now derived from {@code (seed, cell)} and realized on arrival like + * every other world in the game.

    + * + *

    The authored bodies always win: their cells are handed to the derivation as already taken, so + * nothing derived can land on one. A system that asks for none is untouched.

    + */ + private List withDerivedRetinue(GalacticCoord anchor, StellarBody star, int starId, + List authored) { + int asked = star.getMaxRetinueBodies(); + if (asked <= 0 || generator == null) { + return authored; + } + Set taken = new HashSet<>(); + for (SystemBody b : authored) { + taken.add(b.name().cellKey()); + } + List all = new ArrayList<>(authored); + all.addAll(generator.authoredRetinueFor(worldSeed, anchor, star, starId, asked, taken)); + return all; + } } diff --git a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java index d2a9cd86a..cc34a8e2e 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java +++ b/src/main/java/zmaster587/advancedRocketry/util/AstronomicalBodyHelper.java @@ -64,6 +64,50 @@ public class AstronomicalBodyHelper { */ public static final long BLOCKS_PER_ORBIT_UNIT = BLOCKS_PER_AU / DISTANCE_UNITS_PER_AU; + /** + * The smallest orbit, in {@code orbitalDistance} units, that can carry an ADDRESS of its own — + * one cell's worth. A body closer in than this shares its star's cell, and a cell is a + * destination: two bodies in one would be one indistinguishable address that neither a jump nor + * an arrival could resolve. + * + *

    So it is the addressing granularity of the whole universe layer, and it is derived from + * the cell edge, never picked. It used to be picked — the companion band's floor was a + * literal {@code 1} with a comment saying it was one cell's worth, which was true at a 4M cell + * (0.67 units) and stopped being true the moment the cell grew. A number whose javadoc states a + * derivation should BE that derivation.

    + * + *

    What it costs, said plainly: a bigger cell buys reach and spends inner resolution. At a 32M + * cell this is 6 units = 0.06 AU, so a contact binary or a body orbiting closer than that cannot + * be a separate destination — it is not generated rather than being generated unreachable.

    + */ + public static final int MIN_ADDRESSABLE_ORBIT_UNITS = (int) Math.max(1L, + (zmaster587.advancedRocketry.space.GalacticCoord.CELL + BLOCKS_PER_ORBIT_UNIT - 1L) + / BLOCKS_PER_ORBIT_UNIT); + + /** + * Earth radii in one SOLAR radius (696 340 km / 6 378 km). A star states its size in solar radii + * and every other body in Earth radii, so anything that draws them on one scale needs this. + */ + public static final double EARTH_RADII_PER_SOLAR_RADIUS = 109.17d; + + /** + * A star's radius in EARTH radii — the unit the render feed sizes every body in. + * + *

    {@code StellarBody.getSize()} is in solar radii, so a star fed straight into a body-sized + * channel would be drawn a hundred times too small. A star with no stated size falls back to one + * solar radius rather than to zero: a sun that vanishes is worse than a sun of the wrong size.

    + */ + public static double starRadiusEarths(zmaster587.advancedRocketry.api.dimension.solar.StellarBody star) { + if (star == null) { + return EARTH_RADII_PER_SOLAR_RADIUS; + } + double solarRadii = star.getSize(); + if (Double.isNaN(solarRadii) || solarRadii <= 0d) { + solarRadii = 1d; + } + return solarRadii * EARTH_RADII_PER_SOLAR_RADIUS; + } + /** Earth's equatorial radius in metres — the unit a body's {@code radius} is stated in. */ public static final double EARTH_RADIUS_METRES = 6_378_137d; /** Earth's radius in chart blocks: what one unit of a body's radius is worth on the chart. */ diff --git a/src/main/resources/assets/advancedrocketry/lang/en_US.lang b/src/main/resources/assets/advancedrocketry/lang/en_US.lang index 0127d099a..3897e2a71 100644 --- a/src/main/resources/assets/advancedrocketry/lang/en_US.lang +++ b/src/main/resources/assets/advancedrocketry/lang/en_US.lang @@ -337,9 +337,6 @@ commands.advancedrocketry.planet.list.entry=DIM%d: %s commands.advancedrocketry.planet.delete.usage=planet delete commands.advancedrocketry.planet.delete.success=Dim %d deleted! commands.advancedrocketry.planet.delete.invalid=World still has players: -commands.advancedrocketry.planet.generate.usage=planet generate [moon] [gas] [atmosphere base] [distance base] [gravity base] -commands.advancedrocketry.planet.generate.invalid=Dimension: %s failed to generate! -commands.advancedrocketry.planet.generate.success=Dimension: %s generated! commands.advancedrocketry.planet.set.usage=planet set [dimId] commands.advancedrocketry.planet.set.success=Successfully set dimension %d's property %s to %s commands.advancedrocketry.planet.set.invalid=Property lookup failed, please check logs @@ -1707,3 +1704,6 @@ msg.navcomputer.eta=Flight time: msg.navcomputer.flightcost=Energy for the flight: msg.navcomputer.hullexposed=Hull outside the window (blocks): msg.navcomputer.ready=Ready to jump +commands.advancedrocketry.planet.generate.usage=planet generate [moon] +commands.advancedrocketry.planet.generate.invalid=Dimension: %s failed to generate! +commands.advancedrocketry.planet.generate.success=Dimension: %s generated! diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java index dad782583..15b4336d3 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/BoundarySkyRendersInSlotCellE2ETest.java @@ -140,14 +140,24 @@ public class BoundarySkyRendersInSlotCellE2ETest extends AbstractClientE2ETest { * feed carries both kinds and so must the subject.

    */ private static final String[][] SYSTEM = { - {"768", "-1072", "-2652", "MOON", "0"}, // ~2 961 - the nearest descend target - {"-23443", "11940", "10363", "MOON", "0"}, // ~28 275 - {"-30108", "-13988", "11037", "MOON", "0"}, // ~34 985 - {"7644", "34614", "-16382", "GAS_GIANT", "-1"}, // ~39 050 - not a descend target - {"-42912", "-23517", "-24475", "MOON", "0"}, // ~54 713 - {"-39818", "28442", "-33418", "MOON", "0"}, // ~59 255 + {"768", "-1072", "-2652", "MOON", "0", "0.27"}, // ~2 961 - the nearest descend target + {"-23443", "11940", "10363", "MOON", "0", "0.27"}, // ~28 275 + {"-30108", "-13988", "11037", "MOON", "0", "0.27"}, // ~34 985 + {"7644", "34614", "-16382", "GAS_GIANT", "-1", "11.0"}, // ~39 050 - not a descend target + {"-42912", "-23517", "-24475", "MOON", "0", "0.27"}, // ~54 713 + {"-39818", "28442", "-33418", "MOON", "0", "0.27"}, // ~59 255 }; + /** A body's radius in Earth radii, as the fixture states it — the sixth column above. */ + private static double radiusEarths(int index) { + return Double.parseDouble(SYSTEM[index][5]); + } + + /** The same, in the chart blocks the feed sends and the renderer sizes with. */ + private static double radiusBlocks(int index) { + return radiusEarths(index) * zmaster587.advancedRocketry.util.AstronomicalBodyHelper.EARTH_RADIUS_BLOCKS; + } + /** The nearest descend target: the body a pilot has to find and fly at to descend at all. */ private static final int NEAREST = 0; /** The gas giant: a non-descend body, which takes the other tint and no texture of its own. */ @@ -318,8 +328,11 @@ public void aPilotInASlotCellSeesTheBodiesAndStars() throws Exception { emptyBefore = capture(slotDim, CELL_CAPTURE_Y, EMPTY_YAW, EMPTY_PITCH, "before_empty"); for (String[] body : SYSTEM) { + // The radius is stated, not implied: since 2026-08-16 the sky sizes a body by the + // ANGLE it subtends, so a fixture that named no radius would draw six identical + // markers and the size legs below would be measuring nothing. String poi = exec("artest space add-poi " + cell + " " + body[0] + " " + body[1] + " " - + body[2] + " " + body[3] + " " + body[4] + " 7"); + + body[2] + " " + body[3] + " " + body[4] + " 7 " + body[5]); assertTrue("add-poi must register the body: " + poi, poi.contains("\"ok\":true")); } @@ -661,7 +674,8 @@ private void assertBodyDrawn(int index, BufferedImage beforeFrame, BufferedImage * the client actually drew, so a build that drew nothing fails whatever the box is. */ private static double discRadiusOf(int index) { - return Math.toDegrees(Math.atan(ApparentSize.halfSizeFor(distanceOf(index)) / 90.0)) / 70.0; + return Math.toDegrees(Math.atan( + ApparentSize.halfSizeFor(radiusBlocks(index), distanceOf(index)) / 90.0)) / 70.0; } /** How far the configured body {@code index} is from the settled ship, in blocks. */ diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java index 71318b892..831b6db69 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java @@ -20,7 +20,7 @@ /** * SPIKE — is the render actually quantized far from the origin, the thing the 4M cell was sized for? * - *

    `space-model.md` justifies `CELL = 4_000_000` with "entity doubles / chunks / lighting degrade + *

    The cell used to be 4,000,000 blocks, justified with "entity doubles / chunks / lighting degrade * past ~±2M blocks in 1.12.2". The server half of that (chunk generation, block storage) measured * CLEAN out to 28M. This measures the visual half. * diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/VehicleRideClientGroupE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/VehicleRideClientGroupE2ETest.java index 00cb066f3..93d71bab4 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/VehicleRideClientGroupE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/VehicleRideClientGroupE2ETest.java @@ -72,8 +72,8 @@ public class VehicleRideClientGroupE2ETest extends AbstractSharedClientE2ETest { private static final Pattern ENTITY_ID = Pattern.compile("\"entityId\":(-?\\d+)"); private static final Pattern RIDING_ID = Pattern.compile("\"ridingEntityId(?:Now)?\":(-?\\d+)"); - private static final Pattern POS_X = Pattern.compile("\"posX\":(-?\\d+(?:\\.\\d+)?)"); - private static final Pattern POS_Z = Pattern.compile("\"posZ\":(-?\\d+(?:\\.\\d+)?)"); + private static final Pattern POS_X = Pattern.compile("\"posX\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); + private static final Pattern POS_Z = Pattern.compile("\"posZ\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); @Override protected String subsystem() { diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandClientGroupE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandClientGroupE2ETest.java index 47e90d30b..ef7ccaeb7 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandClientGroupE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandClientGroupE2ETest.java @@ -60,7 +60,7 @@ public class WorldCommandClientGroupE2ETest extends AbstractSharedClientE2ETest private static final Pattern DIM_LINE = Pattern.compile("DIM(\\d+):"); private static final Pattern PLAYER_NAME = Pattern.compile("\"player\":\"([^\"]+)\""); private static final Pattern STATION_ID = Pattern.compile("\"id\":(-?\\d+)"); - private static final Pattern POS_X = Pattern.compile("\"posX\":(-?\\d+(?:\\.\\d+)?)"); + private static final Pattern POS_X = Pattern.compile("\"posX\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); /** The space dim, where {@code /ar goto station} lands the player. */ private static final int SPACE_DIM = -2; @@ -241,7 +241,7 @@ public void arGotoTransfersPlayerToTargetDim() throws Exception { scenario().arranging("op the bot and generate a planet to travel to"); opTheBot(); String before = exec("ar planet list"); - exec("ar planet generate 0 GotoTarget 10 10 10"); + exec("ar planet generate 0 GotoTarget"); String after = exec("ar planet list"); int targetDim = newDimFromDiff(before, after); scenario().record("targetDim", targetDim); @@ -284,7 +284,7 @@ public void arGotoMakesTheClientRenderThePlanetsOwnWorldType() throws Exception + " comparison below has nothing to change FROM; got '" + home + "'", !home.isEmpty()); String before = exec("ar planet list"); - exec("ar planet generate 0 WorldTypeTarget 10 10 10"); + exec("ar planet generate 0 WorldTypeTarget"); String after = exec("ar planet list"); int targetDim = newDimFromDiff(before, after); scenario().record("targetDim", targetDim); diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandFetchModeratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandFetchModeratorTest.java index 54a7f2e0e..46f21a52f 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandFetchModeratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/WorldCommandFetchModeratorTest.java @@ -53,8 +53,8 @@ public class WorldCommandFetchModeratorTest { private static final String BOT1_NAME = "ModBot1"; private static final String BOT2_NAME = "ModBot2"; - private static final Pattern PLAYER_POS_X = Pattern.compile("\"playerPosX\":(-?\\d+(?:\\.\\d+)?)"); - private static final Pattern PLAYER_POS_Z = Pattern.compile("\"playerPosZ\":(-?\\d+(?:\\.\\d+)?)"); + private static final Pattern PLAYER_POS_X = Pattern.compile("\"playerPosX\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); + private static final Pattern PLAYER_POS_Z = Pattern.compile("\"playerPosZ\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); private RealDedicatedServerHarness server; private RealClientHarness bot1Harness; diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/BeaconEnableCycleTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/BeaconEnableCycleTest.java index 7b6e0e268..85c5cbe2a 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/BeaconEnableCycleTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/BeaconEnableCycleTest.java @@ -70,7 +70,7 @@ public class BeaconEnableCycleTest extends AbstractSharedServerTest { @BeforeClass public static void generateSharedPlanet() throws Exception { Set before = arDims(); - exec("ar planet generate 0 BeaconPhase3 10 10 10"); + exec("ar planet generate 0 BeaconPhase3"); Set diff = arDims(); diff.removeAll(before); assertTrue("planet generate must add exactly one dim — diff=" + diff, diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/InterstellarJumpLegE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/InterstellarJumpLegE2ETest.java index 0a6f7876d..12a0708c0 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/InterstellarJumpLegE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/InterstellarJumpLegE2ETest.java @@ -254,7 +254,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/LowGravFallDamageTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/LowGravFallDamageTest.java index 37bb5423b..fdf325068 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/LowGravFallDamageTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/LowGravFallDamageTest.java @@ -29,9 +29,9 @@ public class LowGravFallDamageTest { private static final int DIM_LOW_GRAV = 9701; private static final Pattern IS_PLANETARY = Pattern.compile("\"isPlanetaryProvider\":(true|false)"); - private static final Pattern INPUT_DIST = Pattern.compile("\"inputDistance\":(-?\\d+(?:\\.\\d+)?)"); - private static final Pattern RESULT_DIST = Pattern.compile("\"resultDistance\":(-?\\d+(?:\\.\\d+)?)"); - private static final Pattern GRAVITY = Pattern.compile("\"gravityMultiplier\":(-?\\d+(?:\\.\\d+)?)"); + private static final Pattern INPUT_DIST = Pattern.compile("\"inputDistance\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); + private static final Pattern RESULT_DIST = Pattern.compile("\"resultDistance\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); + private static final Pattern GRAVITY = Pattern.compile("\"gravityMultiplier\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); private Path workDir; private RealDedicatedServerHarness harness; diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/ParkedShipKeepsItsBodiesE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/ParkedShipKeepsItsBodiesE2ETest.java index e3a535995..af303de59 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/ParkedShipKeepsItsBodiesE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/ParkedShipKeepsItsBodiesE2ETest.java @@ -74,7 +74,6 @@ public void aBodyStaysInItsOwnCellAcrossAVeryLongDwell() throws Exception { before.contains("\"dim\":" + WATCHED_DIM + ",\"kind\"")); String frameBefore = exec("artest space frame " + cellArgs); - long originBefore = jsonLong(frameBefore, "originX"); long clockBefore = jsonLong(frameBefore, "clock"); String after; @@ -106,15 +105,19 @@ public void aBodyStaysInItsOwnCellAcrossAVeryLongDwell() throws Exception { exec("artest space set-clock " + clockBefore); } - // THE CONTROL. - long originAfter = jsonLong(frameAfter, "originX"); + // THE CONTROL. The frame's origin is reported as a SECTOR triple plus an in-cell offset, so the + // move has to be reassembled from both: the probe never emitted a flat "originX", and reading + // one asserted nothing while looking like it asserted everything — this leg failed with "no + // numeric originX" rather than with anything about the universe, and had done so silently. + long movedX = frameMoveX(frameBefore, frameAfter); assertNotEquals("the cell's FRAME must have moved over " + AGE_TICKS + " ticks, or the" + " invariance below is a statement about a universe that stands still; before=" - + frameBefore + " after=" + frameAfter, originBefore, originAfter); - assertTrue("...and moved FAR — a cell is 4,000,000 blocks wide, so a smaller move would not" - + " even have left the cell under the old derivation; moved=" - + Math.abs(originAfter - originBefore), - Math.abs(originAfter - originBefore) > 4_000_000L); + + frameBefore + " after=" + frameAfter, 0L, movedX); + assertTrue("...and moved FAR — further than a whole cell (" + + zmaster587.advancedRocketry.space.GalacticCoord.CELL + + " blocks), so the frame cannot be said to have merely drifted inside one;" + + " moved=" + Math.abs(movedX), + Math.abs(movedX) > zmaster587.advancedRocketry.space.GalacticCoord.CELL); // THE CLAUSE. Same cell key, same occupants, same count. assertEquals("a body's own cell may not change because time passed (ledger #143): " + after, @@ -136,6 +139,29 @@ public void aBodyStaysInItsOwnCellAcrossAVeryLongDwell() throws Exception { // --- helpers --------------------------------------------------------------------------------- + /** + * How far the cell frame's origin moved along X between two {@code space frame} replies, in + * blocks. The reply carries {@code originSector} and {@code originOffset}, and the answer needs + * both — a frame that crossed a cell face has a small offset delta and a whole cell of real + * movement hiding in the sector. + */ + private static long frameMoveX(String before, String after) { + long sectorDelta = jsonArrayElement(after, "originSector", 0) + - jsonArrayElement(before, "originSector", 0); + long offsetDelta = jsonArrayElement(after, "originOffset", 0) + - jsonArrayElement(before, "originOffset", 0); + return sectorDelta * zmaster587.advancedRocketry.space.GalacticCoord.CELL + offsetDelta; + } + + /** Element {@code index} of a numeric JSON array field. */ + private static long jsonArrayElement(String json, String field, int index) { + Matcher m = Pattern.compile("\"" + Pattern.quote(field) + "\":\\[([^\\]]*)\\]").matcher(json); + assertTrue("probe response carries no \"" + field + "\" array: " + json, m.find()); + String[] parts = m.group(1).split(","); + assertTrue("\"" + field + "\" has no element " + index + ": " + json, parts.length > index); + return Long.parseLong(parts[index].trim()); + } + private static String dimCell(String json) { Matcher m = Pattern.compile("\"dimCell\":\"([^\"]+)\"").matcher(json); assertTrue("probe response carries no \"dimCell\": " + json, m.find()); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/PlanetGenerateMoonNullStarTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/PlanetGenerateMoonNullStarTest.java deleted file mode 100644 index 4d24acb81..000000000 --- a/src/test/java/zmaster587/advancedRocketry/test/server/PlanetGenerateMoonNullStarTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package zmaster587.advancedRocketry.test.server; - -import org.junit.Test; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * MED batch pack 4 — C072 reproduction + regression guard. - * - *

    Contract under test: {@code /advancedrocketry planet generate moon …} - * must fail with a clean {@code CommandException} — not an unguarded - * {@link NullPointerException} — when the parent planet's star id resolves to no - * star. The non-moon path guards {@code getStar(id) == null} - * ({@code PlanetGenerateCommand} else-if), but the moon path re-derives the star - * id from the parent planet and skips that guard, then feeds it to - * {@code generateRandom} (which dereferences {@code getStar}) — an op-only command - * crash.

    - * - *

    The probe drives the REAL command's {@code execute} against a planet whose - * star has been temporarily orphaned, and reports the thrown type. Pre-fix it is - * a {@code NullPointerException}; post-fix a star-existence guard on the moon - * branch throws {@code CommandException} before any generation, and no dimension - * is registered in either case.

    - */ -public class PlanetGenerateMoonNullStarTest extends AbstractSharedServerTest { - - private static final Pattern AR_DIMS = Pattern.compile("\"arDimensions\":\\[([^\\]]*)]"); - private static final Pattern THROWN = Pattern.compile("\"thrown\":\"([^\"]*)\""); - private static final Pattern DIMS_BEFORE = Pattern.compile("\"dimsBefore\":(-?\\d+)"); - private static final Pattern DIMS_AFTER = Pattern.compile("\"dimsAfter\":(-?\\d+)"); - - private static String ok(java.util.List resp) { - return String.join("\n", resp); - } - - /** Pick a registered AR planet dimension (positive id, non-overworld) to - * serve as the parent planet for the moon-generate. */ - private int anyArPlanetDim() throws Exception { - String list = ok(client().execute("artest dim list")); - Matcher m = AR_DIMS.matcher(list); - assertTrue("dim list missing arDimensions: " + list, m.find()); - String[] ids = m.group(1).split(","); - for (String id : ids) { - String s = id.trim(); - if (s.isEmpty()) continue; - int d = Integer.parseInt(s); - if (d > 0) return d; - } - throw new IllegalStateException("no positive AR planet dim in: " + list); - } - - @Test - public void moonGenerateWithOrphanStarThrowsCleanlyNotNpe() throws Exception { - int planetDim = anyArPlanetDim(); - - String resp = ok(client().execute("artest planet moon-generate-catch " + planetDim)); - assertTrue("moon-generate-catch failed: " + resp, resp.contains("\"ok\":true")); - - Matcher tm = THROWN.matcher(resp); - assertTrue("thrown field missing: " + resp, tm.find()); - String thrown = tm.group(1); - assertFalse("generating a moon for a planet whose star resolves to no star " - + "must not NPE (C072); got " + thrown + ": " + resp, - "NullPointerException".equals(thrown)); - assertTrue("the moon-generate must fail with a clean CommandException, " - + "got " + thrown + ": " + resp, - "CommandException".equals(thrown)); - - // The guard must fire before any generation — no dimension registered. - Matcher bm = DIMS_BEFORE.matcher(resp); - Matcher am = DIMS_AFTER.matcher(resp); - assertTrue("dimsBefore missing: " + resp, bm.find()); - assertTrue("dimsAfter missing: " + resp, am.find()); - assertTrue("no dimension may be registered when the guard rejects the " - + "command: " + resp, - Integer.parseInt(bm.group(1)) == Integer.parseInt(am.group(1))); - } -} diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/ShipArrivalKeepsItsPilotSeatInASuperheatedAtmosphereTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/ShipArrivalKeepsItsPilotSeatInASuperheatedAtmosphereTest.java index 4f89b1a89..6f9ffc369 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/ShipArrivalKeepsItsPilotSeatInASuperheatedAtmosphereTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/ShipArrivalKeepsItsPilotSeatInASuperheatedAtmosphereTest.java @@ -215,7 +215,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/TerraformerPoweredCycleOnArPlanetTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/TerraformerPoweredCycleOnArPlanetTest.java index 9456980eb..ebb4e9474 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/TerraformerPoweredCycleOnArPlanetTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/TerraformerPoweredCycleOnArPlanetTest.java @@ -68,9 +68,10 @@ public class TerraformerPoweredCycleOnArPlanetTest extends AbstractSharedServerT @Before public void generatePlanet() throws Exception { Set before = arDims(); - // Args 10 10 10 are positive randomness factors — see - // WorldCommandPlanetLifecycleContractTest for the same idiom. - exec("ar planet generate 0 Phase1aTerraformer 10 10 10"); + // The world is DERIVED, not rolled: /ar planet generate lost its three randomness arguments + // with the legacy generator behind them, so the same command on the same seed now mints the + // same planet. + exec("ar planet generate 0 Phase1aTerraformer"); Set diff = arDims(); diff.removeAll(before); assertEquals("planet generate must add exactly one dim — diff=" + diff, @@ -125,7 +126,17 @@ public void nativePlanetTerraformerWithFuelAndPowerStepsDensity() throws Excepti assertTrue("controller-state probe missing batteries readout — " + preState, preState.contains("\"batteriesPresent\":true")); + // ARRANGE the starting density instead of taking whatever the world hands over. The + // terraformer only steps UP while density is below its ceiling of 1600, and a planet's + // pressure is now DERIVED from its own physics — so a fixture that inherits it can be handed + // a world already AT the ceiling and then measures nothing. (It was: this leg failed with + // before=1600 after=1600 while its two siblings passed, which is what a fixture at the + // ceiling looks like, not a terraformer that does not work.) The subject is whether a + // powered, fuelled terraformer MOVES the density; where it starts is arrangement. + exec("ar planet set " + newDim + " atmosphereDensity 100"); int densityBefore = readDensity(); + assertTrue("arrangement: the planet must start below the terraformer's ceiling, got " + + densityBefore, densityBefore < 1600); // Refill loop: terraformer needs BOTH N2 and O2 each tick. // TileFluidHatch holds one fluid per tank — so split: hatch 0+1 // are N2 sources, hatch 2+3 are O2 sources. The controller's diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSCrossingLeavesNoShipBehindE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSCrossingLeavesNoShipBehindE2ETest.java index ba5f76a93..f82bd7806 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSCrossingLeavesNoShipBehindE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSCrossingLeavesNoShipBehindE2ETest.java @@ -234,7 +234,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSCrossingOutOfAnUnloadedSourceE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSCrossingOutOfAnUnloadedSourceE2ETest.java index e7571e4fc..7a1eacd7b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSCrossingOutOfAnUnloadedSourceE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSCrossingOutOfAnUnloadedSourceE2ETest.java @@ -198,7 +198,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSDoubleQueuedShipLoadDoesNotKillTheServerE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSDoubleQueuedShipLoadDoesNotKillTheServerE2ETest.java index 31f3a8a6d..6815945ad 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSDoubleQueuedShipLoadDoesNotKillTheServerE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSDoubleQueuedShipLoadDoesNotKillTheServerE2ETest.java @@ -188,7 +188,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipAutoTakeoffE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipAutoTakeoffE2ETest.java index 12c96e8ac..51b6495b8 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipAutoTakeoffE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipAutoTakeoffE2ETest.java @@ -167,7 +167,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCrossingSpikeTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCrossingSpikeTest.java index dcd2dcc45..218d45fa5 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCrossingSpikeTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCrossingSpikeTest.java @@ -177,7 +177,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipDescentE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipDescentE2ETest.java index 4dff01731..df3538c1f 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipDescentE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipDescentE2ETest.java @@ -196,7 +196,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipEntryE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipEntryE2ETest.java index 3c2fd2fbb..82a10c47d 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipEntryE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipEntryE2ETest.java @@ -333,7 +333,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSUnpilotedEntryE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSUnpilotedEntryE2ETest.java index df4997038..536032e1a 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSUnpilotedEntryE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSUnpilotedEntryE2ETest.java @@ -185,7 +185,7 @@ private static int extractInt(String json, String key) { } private static double extractDouble(String json, String key) { - Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(json); return m.find() ? Double.parseDouble(m.group(1)) : 0.0; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/WearAccrualDisableTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/WearAccrualDisableTest.java index c883387b2..75d221ee3 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/WearAccrualDisableTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/WearAccrualDisableTest.java @@ -25,7 +25,7 @@ public class WearAccrualDisableTest extends AbstractSharedServerTest { Pattern.compile("\"builderPos\":\\[(-?\\d+),(-?\\d+),(-?\\d+)]"); private static final Pattern ROCKET_LIST_ID = Pattern.compile("\"id\":(-?\\d+)"); private static final Pattern BREAKING_PROB = - Pattern.compile("\"breakingProb\":(-?\\d+(?:\\.\\d+)?)"); + Pattern.compile("\"breakingProb\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); private String cmd(String c) throws Exception { return String.join("\n", client().execute(c)); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/WearSystemTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/WearSystemTest.java index 9f785d652..47d5c762b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/WearSystemTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/WearSystemTest.java @@ -100,7 +100,7 @@ public void wearStageRoundTripsThroughCapability() throws Exception { private double breakingProbOf(int entityId) throws Exception { String info = String.join("\n", client().execute("artest rocket info " + entityId)); - Matcher m = Pattern.compile("\"breakingProb\":(-?\\d+(?:\\.\\d+)?)").matcher(info); + Matcher m = Pattern.compile("\"breakingProb\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)").matcher(info); assertTrue("no breakingProb in info: " + info, m.find()); return Double.parseDouble(m.group(1)); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/WeightSystemTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/WeightSystemTest.java index 2ce611279..899abe4f6 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/WeightSystemTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/WeightSystemTest.java @@ -29,7 +29,7 @@ */ public class WeightSystemTest extends AbstractSharedServerTest { - private static final Pattern WEIGHT = Pattern.compile("\"weight\":(-?\\d+(?:\\.\\d+)?)"); + private static final Pattern WEIGHT = Pattern.compile("\"weight\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)"); private void reset() throws Exception { String r = String.join("\n", client().execute("artest weight reset")); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/WorldCommandPlanetLifecycleContractTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/WorldCommandPlanetLifecycleContractTest.java index 9ae577a1f..d917a498c 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/WorldCommandPlanetLifecycleContractTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/WorldCommandPlanetLifecycleContractTest.java @@ -43,7 +43,7 @@ private static Set dimIds() throws Exception { @Test public void planetGenerateAddsExactlyOneEntryToRegistry() throws Exception { Set before = dimIds(); - exec("ar planet generate 0 GenTestA 10 10 10"); + exec("ar planet generate 0 GenTestA"); Set after = dimIds(); try { after.removeAll(before); @@ -57,7 +57,7 @@ public void planetGenerateAddsExactlyOneEntryToRegistry() throws Exception { @Test public void planetGenerateNamesNewDimensionFromArg() throws Exception { Set before = dimIds(); - exec("ar planet generate 0 GenTestNamed 10 10 10"); + exec("ar planet generate 0 GenTestNamed"); Set diff = dimIds(); diff.removeAll(before); try { @@ -73,7 +73,7 @@ public void planetGenerateNamesNewDimensionFromArg() throws Exception { @Test public void planetDeleteRemovesEntryFromRegistry() throws Exception { Set before = dimIds(); - exec("ar planet generate 0 GenTestDel 10 10 10"); + exec("ar planet generate 0 GenTestDel"); Set diff = dimIds(); diff.removeAll(before); assertEquals(1, diff.size()); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ApparentSizeTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ApparentSizeTest.java index a07d6d15a..abad1056b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ApparentSizeTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ApparentSizeTest.java @@ -8,53 +8,103 @@ import static org.junit.Assert.assertTrue; /** - * A fed body is drawn at an apparent size that FALLS with distance and is CLAMPED at both ends. + * A fed body is drawn at an apparent size that RISES with the angle it subtends — its own radius over + * the distance to it — and is CLAMPED at both ends. * - *

    Neither half is polish. The fed range runs from a few thousand blocks to ~109, so an - * unclamped inverse law draws the star at a fraction of a pixel; and the renderer drops a body whose - * direction vector is shorter than 10-6, i.e. a body vanishes exactly when it is closest, - * which the maximum is what stops being the only cue. The particular curve and the four numbers are - * {@code tunable} and are deliberately not pinned here.

    + *

    Neither half is polish. Radii run from a small moon to a star and distances from a few thousand + * blocks to ~109, so an unclamped inverse law draws the star at a fraction of a pixel; and + * the renderer drops a body whose direction vector is shorter than 10-6, i.e. a body + * vanishes exactly when it is closest, which the maximum is what stops being the only cue. The + * particular curve and the four numbers are {@code tunable} and are deliberately not pinned here.

    + * + *

    What IS pinned is the shape the sky was missing until 2026-08-16: size used to be a function of + * distance alone, so a moon and a gas giant beside each other drew the same disc.

    */ public class ApparentSizeTest { + /** Earth-ish and Jupiter-ish, in the chart blocks the feed sends. */ + private static final double MOON_R = 6_800d; + private static final double EARTH_R = 25_512d; + private static final double GIANT_R = 280_000d; + @Test - public void sizeFallsAsDistanceGrows() { - double[] distances = {2_000d, 10_000d, 100_000d, 1_000_000d, 10_000_000d, 100_000_000d}; - float previous = ApparentSize.halfSizeFor(distances[0]); + public void sizeFallsAsTheSameBodyRecedes() { + double[] distances = {200_000d, 1_000_000d, 10_000_000d, 100_000_000d, 1_000_000_000d}; + float previous = ApparentSize.halfSizeFor(EARTH_R, distances[0]); for (int i = 1; i < distances.length; i++) { - float now = ApparentSize.halfSizeFor(distances[i]); + float now = ApparentSize.halfSizeFor(EARTH_R, distances[i]); assertTrue("size must fall from " + distances[i - 1] + " to " + distances[i] + " (" + previous + " -> " + now + ")", now < previous); previous = now; } } + @Test + public void aBiggerBodyOutdrawsASmallerOneAtTheSameRange() { + // THE defect this file exists for: with size keyed on distance alone these three were equal. + double range = 5_000_000d; + float moon = ApparentSize.halfSizeFor(MOON_R, range); + float earth = ApparentSize.halfSizeFor(EARTH_R, range); + float giant = ApparentSize.halfSizeFor(GIANT_R, range); + assertTrue("an Earth must outdraw a moon at the same range (" + moon + " vs " + earth + ")", + earth > moon); + assertTrue("a giant must outdraw an Earth at the same range (" + earth + " vs " + giant + ")", + giant > earth); + } + + @Test + public void twoBodiesOfEqualRadiusAtEqualRangeAreDrawnEqual() { + // The contract stated positively: nothing but the pair (radius, distance) may enter, so two + // bodies that agree on both are the same size whatever else differs about them. + assertEquals(ApparentSize.halfSizeFor(EARTH_R, 3_000_000d), + ApparentSize.halfSizeFor(EARTH_R, 3_000_000d), 0f); + } + + @Test + public void onlyTheRATIOMatters() { + // A body twice as big, twice as far, subtends the same angle — so it draws the same. This is + // what makes the argument an angular size rather than two loosely-related numbers. + assertEquals(ApparentSize.halfSizeFor(EARTH_R, 4_000_000d), + ApparentSize.halfSizeFor(2d * EARTH_R, 8_000_000d), 1e-4); + assertEquals(ApparentSize.halfSizeFor(MOON_R, 900_000d), + ApparentSize.halfSizeFor(MOON_R / 10d, 90_000d), 1e-4); + } + @Test public void sizeIsClampedAtBothEnds() { assertEquals("a body on top of you does not fill the sky", ApparentSize.MAX_HALF_SIZE, - ApparentSize.halfSizeFor(0d), 1e-6); - assertEquals(ApparentSize.MAX_HALF_SIZE, ApparentSize.halfSizeFor(1d), 1e-6); + ApparentSize.halfSizeFor(EARTH_R, 1d), 1e-6); assertEquals("a body at the neighbourhood bound is still drawn", - ApparentSize.MIN_HALF_SIZE, ApparentSize.halfSizeFor(1.0e12), 1e-6); + ApparentSize.MIN_HALF_SIZE, ApparentSize.halfSizeFor(EARTH_R, 1.0e15), 1e-6); assertTrue("nothing is ever drawn at zero size", ApparentSize.MIN_HALF_SIZE > 0f); } @Test - public void everyDistanceInTheFedRangeStaysInsideTheClamps() { - // The fed range: a moon in the observer's own cell out to the far side of a neighbourhood. - for (double d = 1d; d < 1.0e10; d *= 3d) { - float half = ApparentSize.halfSizeFor(d); - assertTrue("size left the clamps at " + d + ": " + half, - half >= ApparentSize.MIN_HALF_SIZE && half <= ApparentSize.MAX_HALF_SIZE); + public void everyFedPairStaysInsideTheClamps() { + for (double r : new double[] {1d, MOON_R, EARTH_R, GIANT_R, 2.8e6}) { + for (double d = 1d; d < 1.0e10; d *= 3d) { + float half = ApparentSize.halfSizeFor(r, d); + assertTrue("size left the clamps at r=" + r + " d=" + d + ": " + half, + half >= ApparentSize.MIN_HALF_SIZE && half <= ApparentSize.MAX_HALF_SIZE); + } } } + @Test + public void aBodyWithNoRadiusIsAMarkerNotAGuess() { + // A belt or a station slot is not a sphere. It gets the marker size rather than a size + // invented for it — the guessing that made every body the same disc in the first place. + assertEquals(ApparentSize.MIN_HALF_SIZE, ApparentSize.halfSizeFor(0d, 100_000d), 1e-6); + assertEquals(ApparentSize.MIN_HALF_SIZE, ApparentSize.halfSizeFor(-3d, 100_000d), 1e-6); + assertEquals(ApparentSize.MIN_HALF_SIZE, ApparentSize.halfSizeFor(Double.NaN, 100_000d), 1e-6); + } + @Test public void aNonsenseDistanceIsTreatedAsNearRatherThanInvisible() { // A body whose vector could not be measured must not silently disappear from the sky. - assertEquals(ApparentSize.MAX_HALF_SIZE, ApparentSize.halfSizeFor(Double.NaN), 1e-6); - assertEquals(ApparentSize.MAX_HALF_SIZE, ApparentSize.halfSizeFor(-5d), 1e-6); + assertEquals(ApparentSize.MAX_HALF_SIZE, ApparentSize.halfSizeFor(EARTH_R, Double.NaN), 1e-6); + assertEquals(ApparentSize.MAX_HALF_SIZE, ApparentSize.halfSizeFor(EARTH_R, -5d), 1e-6); + assertEquals(ApparentSize.MAX_HALF_SIZE, ApparentSize.halfSizeFor(EARTH_R, 0d), 1e-6); } @Test diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index eb5056017..31f847dc4 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; /** @@ -566,4 +567,87 @@ private static Set occupiedSeats(ClusteredGalaxyGenerator gen, long seed } return keys; } + + // ─── the retinue an AUTHORED system gets: one generator, never two ───────── + + private static zmaster587.advancedRocketry.api.dimension.solar.StellarBody authoredStar() { + zmaster587.advancedRocketry.api.dimension.solar.StellarBody star = + new zmaster587.advancedRocketry.api.dimension.solar.StellarBody(); + star.setName("Authored"); + star.setId(0); + star.setSize(1f); + star.setTemperature(100); + return star; + } + + @Test + public void anAuthoredSystemsDerivedRetinueIsTheSameEverySave() { + // The whole reason the legacy generator had to go: it drew from + // new Random(System.currentTimeMillis()), so two saves of one seed held different worlds and + // nothing about a system could be predicted, reproduced or reported. + ClusteredGalaxyGenerator g = new ClusteredGalaxyGenerator(defaultsCfg()); + GalacticCoord anchor = cell(0, 0, 0); + + List first = g.authoredRetinueFor(SEED, anchor, authoredStar(), 0, 6, + java.util.Collections.emptySet()); + List again = g.authoredRetinueFor(SEED, anchor, authoredStar(), 0, 6, + java.util.Collections.emptySet()); + + assertEquals("the same seed must produce the same system, body for body", first, again); + assertTrue("...and it must actually produce one", first.size() > 1); + + List otherSeed = g.authoredRetinueFor(SEED + 1L, anchor, authoredStar(), 0, 6, + java.util.Collections.emptySet()); + assertNotEquals("a different seed must produce a different system, or the derivation ignores" + + " its seed and determinism is vacuous", first, otherSeed); + } + + @Test + public void aPacksBodyCountBoundsWhatItsStarGets() { + // The pack-facing knob the legacy generator consumed: a pack that asks for more worlds gets + // more of them. Stated as a bound rather than an equality, because the drawn orbits still + // decide how many FIT — a system squeezed by its neighbours holds fewer worlds rather than + // the same worlds at the wrong distances. + ClusteredGalaxyGenerator g = new ClusteredGalaxyGenerator(defaultsCfg()); + GalacticCoord anchor = cell(0, 0, 0); + + int few = majorBodies(g.authoredRetinueFor(SEED, anchor, authoredStar(), 0, 2, + java.util.Collections.emptySet())); + int many = majorBodies(g.authoredRetinueFor(SEED, anchor, authoredStar(), 0, 10, + java.util.Collections.emptySet())); + + assertTrue("asking for two must not hand out more than two worlds, got " + few, few <= 2); + assertTrue("asking for ten must hand out more than asking for two (" + few + " -> " + many + + ")", many > few); + } + + @Test + public void anAuthoredWorldsCellIsNeverTakenByADerivedOne() { + // The authored system wins: a pack's own world may not be displaced, or shadowed, by a body + // the generator drew. + ClusteredGalaxyGenerator g = new ClusteredGalaxyGenerator(defaultsCfg()); + GalacticCoord anchor = cell(0, 0, 0); + List free = g.authoredRetinueFor(SEED, anchor, authoredStar(), 0, 6, + java.util.Collections.emptySet()); + assertTrue("arrangement: the free draw must place something to reserve", free.size() > 1); + + java.util.Set reserved = new java.util.HashSet<>(); + for (SystemBody b : free) { + reserved.add(b.name().cellKey()); + } + for (SystemBody b : g.authoredRetinueFor(SEED, anchor, authoredStar(), 0, 6, reserved)) { + assertFalse("a derived body landed on a cell the authored system holds: " + + b.name().cellKey(), reserved.contains(b.name().cellKey())); + } + } + + private static int majorBodies(List bodies) { + int n = 0; + for (SystemBody b : bodies) { + if (b.kind() == SystemBodyKind.PLANET || b.kind() == SystemBodyKind.GAS_GIANT) { + n++; + } + } + return n; + } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PacketSystemBodiesSyncTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PacketSystemBodiesSyncTest.java index 28157f65f..af6a0a00e 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PacketSystemBodiesSyncTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PacketSystemBodiesSyncTest.java @@ -37,11 +37,12 @@ public void twoDimsWithDifferingBodyListsRoundTrip() { // A descend target carries a shell; the body beside it carries none. The two must survive // the wire as DIFFERENT numbers — a codec that dropped the field, or wrote one body's value // for every body, would still round-trip a payload where they all agreed. - dimA.add(new RenderBody(2, 100L, -200L, 300L, 41, true, 512L)); - dimA.add(new RenderBody(0, -7L, 8L, -9L, 55, false, 0L)); + dimA.add(new RenderBody(2, 100L, -200L, 300L, 41, true, 512L, 25_512L, RenderBody.NO_PARENT)); + dimA.add(new RenderBody(0, -7L, 8L, -9L, 55, false, 0L, 0L, 0)); List dimB = new ArrayList<>(); - dimB.add(new RenderBody(5, 1_000_000_000_000L, 0L, -1_000_000_000_000L, 7, false, 7_777L)); + dimB.add(new RenderBody(5, 1_000_000_000_000L, 0L, -1_000_000_000_000L, 7, false, 7_777L, + 2_800_000L, RenderBody.NO_PARENT)); sent.put(11, dimA); sent.put(-4, dimB); @@ -118,5 +119,10 @@ private static void assertBody(RenderBody expected, RenderBody actual) { assertEquals("dimId", expected.dimId, actual.dimId); assertEquals("descendTarget", expected.descendTarget, actual.descendTarget); assertEquals("boundaryRadius", expected.boundaryRadius, actual.boundaryRadius); + // The body's OWN size, distinct from the shell around it: the sky cannot draw a giant as a + // giant if this is dropped, and dropping it looks exactly like the old distance-only sizing. + assertEquals("radiusBlocks", expected.radiusBlocks, actual.radiusBlocks); + // Whose moon it is. Dropped, a giant and its retinue arrive as unrelated dots. + assertEquals("parentIndex", expected.parentIndex, actual.parentIndex); } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ShipTransitManagerTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ShipTransitManagerTest.java index 6c97fa084..863b07f84 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ShipTransitManagerTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ShipTransitManagerTest.java @@ -217,8 +217,11 @@ private static SpaceManager.Config never() { return new SpaceManager.Config(SpaceManager.GcPolicy.NEVER, 0, 0); } - // Speed >= the 4,000,000-block inter-cell distance so a single tick arrives; a small speed does not. - private static final long ARRIVE_IN_ONE_TICK = 5_000_000L; + // A speed that covers the inter-cell distance in ONE tick, so an arrival can be asserted without + // ticking a flight out. DERIVED from the cell edge (the same 1.25x margin it always carried), not + // written down: as a literal it silently became a speed that arrives in eight ticks the moment the + // cell grew, and eight of these tests then read as "the arrival never happened". + private static final long ARRIVE_IN_ONE_TICK = GalacticCoord.CELL * 5L / 4L; @Test public void departPutsShipInTransitAndAllocatesALane() { @@ -399,7 +402,11 @@ public void exportTransitsSnapshotsInFlightShips() { assertEquals("the origin is persisted: progress is meaningless without it", cell(1), r.origin); assertEquals(cell(2), r.target); assertEquals("nothing flown yet (not ticked)", 0L, r.travelledBlocks); - assertEquals("the flight is priced at depart, once", 4_000_000L, r.distanceBlocks); + // ONE cell apart, so the price IS the cell edge — bound to the constant, because this is the + // one distance here that is derived rather than chosen. The fixture distances passed to + // importTransit elsewhere in this file are NOT this number: they are magnitudes picked so a + // flight completes inside a test's tick budget, and they merely used to equal it. + assertEquals("the flight is priced at depart, once", GalacticCoord.CELL, r.distanceBlocks); assertEquals(7L, r.speed); assertTrue("no crew captured yet (option-A capture is the VS layer)", r.crew.isEmpty()); } @@ -712,7 +719,7 @@ public void aShipWhoseArrivalHasAlreadyLandedIsNotRecutFromHyperspace() { int originDim = space.materialize(cell(1)); mgr.beginTransit(UUID.randomUUID().toString(), cell(1), originDim, new BlockPos(0, 64, 0), - cell(2), 5_000_000L); + cell(2), ARRIVE_IN_ONE_TICK); assertEquals("control: while it is still parked, a re-cut is exactly what should happen", 1, mgr.refreshSnapshots()); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/StellarHierarchyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/StellarHierarchyTest.java index 9a1de99c9..36acb60ba 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/StellarHierarchyTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/StellarHierarchyTest.java @@ -185,4 +185,64 @@ public void aHierarchyRoundTripsThroughNBTWithItsGeometry() { assertEquals("the geometry survives to the third star", c.separationAuFrom(a), readC.separationAuFrom(read), 1e-9); } + + // ─── the forms the model must be able to express ─────────────────────────── + + @Test + public void anSTypeWorldIsLitByBothStarsOfItsPair() { + // The form: a wide binary whose COMPANION carries the world. It is a planet in a binary, not + // a planet with one sun that happens to have a bright neighbour — so the light it receives + // must include the primary's, and must fall as the pair is drawn apart. + StellarBody primary = star("A", 1f); + primary.setId(3); + StellarBody companion = star("B", 1f); + companion.setId(4); + companion.setOrbitalDistance(200); // 2 AU: a close pair + primary.addSubStar(companion); + + double closePair = AstronomicalBodyHelper.getStellarBrightness(companion, 100); + double lone = AstronomicalBodyHelper.getStellarBrightness(star("C", 1f), 100); + assertTrue("a world of the companion must be lit by the primary too (" + closePair + + " vs a lone star's " + lone + ")", closePair > lone); + + companion.setOrbitalDistance(20_000); // 200 AU: a wide pair + double widePair = AstronomicalBodyHelper.getStellarBrightness(companion, 100); + assertTrue("drawing the pair apart must cost the world the primary's light (" + closePair + + " -> " + widePair + ")", widePair < closePair); + assertTrue("...but never below what its own star alone delivers", widePair >= lone * 0.999d); + } + + @Test + public void aCircumbinaryWorldIsLitByBothStarsOfItsPair() { + // The other form of the same pair: the world is bound to the PRIMARY and the companion is + // one of its suns. Neither arrangement may need a special case. + StellarBody primary = star("A", 1f); + StellarBody companion = star("B", 1f); + companion.setOrbitalDistance(50); + primary.addSubStar(companion); + + double both = AstronomicalBodyHelper.getStellarBrightness(primary, 100); + double alone = AstronomicalBodyHelper.getStellarBrightness(star("C", 1f), 100); + assertTrue("a circumbinary world must be warmed by both (" + both + " vs " + alone + ")", + both > alone); + } + + @Test + public void everyStarOfAThreeStarHierarchyContributesToTheLightAWorldGets() { + // Composition, not enumeration: adding a third star to the system must add its flux from + // wherever it hangs in the tree, or "hierarchical" is a storage claim and nothing more. + StellarBody primary = star("A", 1f); + StellarBody companion = star("B", 1f); + companion.setOrbitalDistance(100); + primary.addSubStar(companion); + double two = AstronomicalBodyHelper.getStellarBrightness(primary, 100); + + StellarBody third = star("C", 1f); + third.setOrbitalDistance(60); + companion.addSubStar(third); + double three = AstronomicalBodyHelper.getStellarBrightness(primary, 100); + + assertTrue("a third star must light the world too (" + two + " -> " + three + ")", + three > two); + } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodyTest.java index e8371b2aa..9336b022f 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodyTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemBodyTest.java @@ -182,4 +182,56 @@ public void kindDescendCapability() { assertFalse(SystemBodyKind.ASTEROID_BELT.canDescend()); assertFalse(SystemBodyKind.STATION_SLOT.canDescend()); } + + @Test + public void aBodyCarriesItsOwnRadiusThroughNbt() { + // A body's SIZE travels with it: nothing downstream can recover it (a procedural world has no + // dimension until a descent mints one, and the render feed reaches a client with no registry). + SystemBody sized = SystemBody.fixedAt(GalacticCoord.ORIGIN, SystemBodyKind.GAS_GIANT, + Constants.INVALID_PLANET, 4).withRadius(11.2d); + NBTTagCompound tag = new NBTTagCompound(); + sized.writeToNBT(tag); + assertEquals(11.2d, SystemBody.readFromNBT(tag).radiusEarths(), 1e-9); + + // A body that is not a sphere says so, and says it the same way after a round trip — the + // renderer draws that as a marker rather than inventing a size for it. + SystemBody belt = SystemBody.fixedAt(GalacticCoord.ORIGIN, SystemBodyKind.ASTEROID_BELT, + Constants.INVALID_PLANET, 4); + NBTTagCompound beltTag = new NBTTagCompound(); + belt.writeToNBT(beltTag); + assertEquals(SystemBody.RADIUS_UNKNOWN, SystemBody.readFromNBT(beltTag).radiusEarths(), 0d); + assertFalse("an unstated radius writes no key at all", beltTag.hasKey("radiusEarths")); + } + + @Test + public void aGiantsMoonSystemSpansFromInsideItsOwnRadiusToBeyondTheCell() { + // The last form the model owes: a giant whose retinue runs from a moon skimming its surface + // out to one that no longer fits in the cell they share. Both must be EXPRESSIBLE, and the + // far one must not corrupt the address — a body outside its own cell would be a body in a + // different cell, so the offset saturates on the face instead (and, since 2026-08-16, says + // so in the log rather than flattening a whole moon system onto one point in silence). + GalacticCoord giantCell = GalacticCoord.ofSectorLocal(9, 0, -3, 0, 0, 0); + CellFrame frame = CellFrame.staticAt(giantCell); + + SystemBody inner = new SystemBody(giantCell, frame, orbit(2d, 100_000L), + SystemBodyKind.MOON, Constants.INVALID_PLANET, 1); + SystemBody outer = new SystemBody(giantCell, frame, + orbit(4d, GalacticCoord.HALF_CELL), SystemBodyKind.MOON, + Constants.INVALID_PLANET, 1); + + assertEquals("both moons share the giant's cell — they are one destination", + inner.name(), outer.name()); + assertNotEquals("and they are not in the same place inside it", + inner.inCellOffsetAt(0L), outer.inCellOffsetAt(0L)); + + for (long tick = 0L; tick < 1000L; tick += 137L) { + long dx = Math.abs(outer.inCellOffsetAt(tick).dx()); + long dy = Math.abs(outer.inCellOffsetAt(tick).dy()); + long dz = Math.abs(outer.inCellOffsetAt(tick).dz()); + assertTrue("an offset may never leave the cell that names it, got " + dx + "," + dy + + "," + dz + " against a half-cell of " + GalacticCoord.HALF_CELL, + dx <= GalacticCoord.HALF_CELL && dy <= GalacticCoord.HALF_CELL + && dz <= GalacticCoord.HALF_CELL); + } + } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java index 0b575ede1..5bead43de 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java @@ -209,7 +209,13 @@ public void aCompanionCostsItsSystemTheWorldsItStandsAmong() { ClusteredGalaxyGenerator g = gen(SPACING); int multiple = 0; int lostSome = 0; - for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { + // A WIDER sweep than its neighbours (3 super-cells, not 2) because this test's thresholds are + // about a proportion — some systems lose worlds, not all of them — and a proportion needs a + // sample. At 2 the sweep returned 28 anchors of which 10 were multiple, i.e. the "more than + // ten multiple systems" arrangement sat exactly ON its own threshold and turned a re-rolled + // universe into a failure about nothing. The rate itself (10/28 = 36 %) is what the + // multiplicity contract says it should be. + for (GalacticCoord anchor : anchors(g, SEED, SPACING, 3)) { if (g.systemAt(SEED, anchor).get().star().getSubStars().isEmpty()) { continue; } @@ -224,7 +230,8 @@ public void aCompanionCostsItsSystemTheWorldsItStandsAmong() { lostSome++; } } - assertTrue("the sweep must find multiple systems", multiple > 10); + assertTrue("the sweep must find multiple systems, saw " + multiple + " of " + + anchors(g, SEED, SPACING, 3).size() + " anchors", multiple > 10); assertTrue("a companion must cost its system something, or the band is not being applied", lostSome > 0); assertTrue("but it must not cost every system everything, saw " + lostSome + "/" + multiple, diff --git a/valkyrienskies/src/main/java/org/valkyrienskies/mod/common/ships/chunk_claims/ShipChunkAllocator.java b/valkyrienskies/src/main/java/org/valkyrienskies/mod/common/ships/chunk_claims/ShipChunkAllocator.java index a749deecd..d74197e4a 100644 --- a/valkyrienskies/src/main/java/org/valkyrienskies/mod/common/ships/chunk_claims/ShipChunkAllocator.java +++ b/valkyrienskies/src/main/java/org/valkyrienskies/mod/common/ships/chunk_claims/ShipChunkAllocator.java @@ -27,7 +27,23 @@ public class ShipChunkAllocator { */ public static final int MAX_CHUNK_LENGTH = 3200; // Who even really cares tbh public static final int MAX_CHUNK_RADIUS = (MAX_CHUNK_LENGTH / 2) - 1; - public static final int CHUNK_X_START = 320000; + /** + * Where the reserved shipyard begins, in chunks. Raised from upstream's 320000 (block X + * 5 094 416 once {@link #MAX_CHUNK_RADIUS} is taken off) to 1 200 000 (block X 19 174 416), + * because {@link #isChunkInShipyard} is what a teleport into the region is silently cancelled + * by — so this constant, not anything in vanilla, is the wall that bounds how far a ship may be + * posed from the origin. It is paired with {@code GalacticCoord.CELL}: a 16M half-cell needs + * clearance to 16M plus room to manoeuvre, and this leaves 3.17M of it. + * + *

    Note the asymmetry the move does NOT fix: the predicate is a half-PLANE, so it reserves the + * whole quadrant out to the world edge while the allocator only ever walks a strip in +Z.

    + * + *

    Timing. The allocator's cursor ({@code lastChunkX}/{@code lastChunkZ}) is SERIALIZED + * into the world, and this constant is not — so a world created before this change restores the + * old cursor and keeps allocating outside the new predicate. The move therefore has to land + * before the release ships, not merely "sometime under the clean break".

    + */ + public static final int CHUNK_X_START = 1200000; public static final int CHUNK_Z_START = 0; private int lastChunkX = CHUNK_X_START; private int lastChunkZ = CHUNK_Z_START; From 3b3bae6bf1e19f97ddb272b9b52c2e9cd29e8649 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 10:23:12 +0300 Subject: [PATCH 25/42] feat: a ship that flies past its cell face is carried across it - add CellSeam: when a pose has left, and where it lands - add CellSeamController: destination taken before source released - ask the carry before reporting position; saturation is now refusal - margins are cell fractions, so the hysteresis rests on no speed - add seam-carry probe, since a slot world does not tick its tiles --- .../command/test/TestProbeCommand.java | 64 ++++ .../advancedRocketry/space/CellSeam.java | 116 ++++++ .../space/CellSeamController.java | 186 ++++++++++ .../space/SpaceSubsystem.java | 8 + .../space/SpaceSubsystemEvents.java | 2 + .../tile/TileAdvancedFlightComputer.java | 29 +- .../assets/advancedrocketry/lang/en_US.lang | 3 + .../assets/advancedrocketry/lang/ru_RU.lang | 3 + .../test/server/VSShipCellSeamE2ETest.java | 334 ++++++++++++++++++ .../test/unit/CellSeamTest.java | 131 +++++++ 10 files changed, 869 insertions(+), 7 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/space/CellSeam.java create mode 100644 src/main/java/zmaster587/advancedRocketry/space/CellSeamController.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/server/VSShipCellSeamE2ETest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/CellSeamTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index b4331afc1..0735da4d3 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -4522,6 +4522,70 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] send(sender, "{\"ok\":true,\"started\":" + started + ",\"pending\":" + d.descendingCount() + "}"); return; } + // seam-carry : drive the PRODUCTION cell-seam carry for the settled ship in that slot + // world — the counterpart of descent-begin, and for the same reason. The trigger lives in the + // flight computer's own tick, which a headless slot world does not run (no player, no ticking + // chunks there), so an e2e that waited for it would be measuring chunk-ticking rather than the + // crossing. WHEN a carry fires is pinned deterministically by CellSeamTest; this verb exists so + // the crossing itself — materialize, cut, paste, settle, ledger handoff — can be exercised on a + // real ship. The ship's LIVE pose is used, never the ledger's: past the face the ledger's copy + // is saturated, so a lookup from it would miss the ship by the whole overshoot. + if (args.length >= 2 && "seam-carry".equalsIgnoreCase(args[0])) { + zmaster587.advancedRocketry.space.CellSeamController seamCtl = + zmaster587.advancedRocketry.space.SpaceSubsystem.seam(); + zmaster587.advancedRocketry.space.ShipLedger seamLedger = + zmaster587.advancedRocketry.space.SpaceSubsystem.ledger(); + if (seamCtl == null || seamLedger == null) { + send(sender, "{\"error\":\"space subsystem not registered\"}"); + return; + } + int slotDim = parseIntOr(args[1], Integer.MIN_VALUE); + net.minecraft.world.WorldServer slotWorld = + net.minecraftforge.common.DimensionManager.getWorld(slotDim); + if (slotWorld == null) { + send(sender, "{\"error\":\"slot world not loaded\",\"slotDim\":" + slotDim + "}"); + return; + } + for (java.util.Map.Entry e + : seamLedger.snapshot().entrySet()) { + zmaster587.advancedRocketry.space.ShipLedger.Entry entry = e.getValue(); + if (entry.state != zmaster587.advancedRocketry.space.ShipLedger.State.SETTLED + || slotDimOfCell(entry.coord) != slotDim) { + continue; + } + double[] ledgerPose = + zmaster587.advancedRocketry.space.CellWorldMapper.poseWorldOf(entry.coord); + double[] live = zmaster587.advancedRocketry.integration.vs.VSIntegration + .nearestShipState(slotWorld, ledgerPose[0], ledgerPose[1], ledgerPose[2], + zmaster587.advancedRocketry.space.GalacticCoord.CELL); + if (live == null) { + send(sender, "{\"ok\":true,\"started\":false,\"reason\":\"no loaded ship near the " + + "ledger pose\",\"shipId\":\"" + e.getKey() + "\"}"); + return; + } + net.minecraft.util.math.BlockPos afc = zmaster587.advancedRocketry.integration.vs + .VSIntegration.flightComputerAt(slotWorld, live[0], live[1], live[2]); + if (afc == null) { + send(sender, "{\"ok\":true,\"started\":false,\"reason\":\"ship carries no flight " + + "computer\",\"shipId\":\"" + e.getKey() + "\"}"); + return; + } + boolean wouldCarry = zmaster587.advancedRocketry.space.CellSeam + .shouldCarry(live[0], live[1], live[2]); + boolean started = seamCtl.requestCarry(slotDim, afc, e.getKey(), entry.coord, + new double[]{live[0], live[1], live[2]}); + send(sender, "{\"ok\":true,\"started\":" + started + + ",\"wouldCarry\":" + wouldCarry + + ",\"shipId\":\"" + e.getKey() + "\"" + + ",\"fromCell\":\"" + entry.coord.cellKey() + "\"" + + ",\"pose\":[" + live[0] + "," + live[1] + "," + live[2] + "]" + + ",\"afc\":[" + afc.getX() + "," + afc.getY() + "," + afc.getZ() + "]}"); + return; + } + send(sender, "{\"ok\":true,\"started\":false,\"reason\":\"no settled ship in this slot\"" + + ",\"slotDim\":" + slotDim + "}"); + return; + } // descent-status: the in-flight descent count (settle progress). if (args.length >= 1 && "descent-status".equalsIgnoreCase(args[0])) { zmaster587.advancedRocketry.space.DescentController d = diff --git a/src/main/java/zmaster587/advancedRocketry/space/CellSeam.java b/src/main/java/zmaster587/advancedRocketry/space/CellSeam.java new file mode 100644 index 000000000..10cced275 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/space/CellSeam.java @@ -0,0 +1,116 @@ +package zmaster587.advancedRocketry.space; + +/** + * The cell face, read as something a ship can FLY THROUGH. + * + *

    A cell's local range is finite, so a ship under sustained thrust reaches its face. Two answers + * were possible: stop it, or carry it. This class holds the second one's arithmetic — when a pose + * has left its cell far enough to count, and where the ship belongs in the neighbour it entered. + * Nothing here touches Minecraft, a world or a ledger; that is {@link CellSeamController}'s work.

    + * + *

    Why a margin exists at all

    + * + *

    A face is a mathematical plane, and a ship loitering ON one would otherwise re-decide its cell + * every tick, ping-ponging between two worlds and paying a full cut-and-paste each time. So the + * crossing arms only once the pose is {@link #CARRY_MARGIN} PAST the face, and the ship is placed + * {@link #REENTRY_DEPTH} inside the neighbour rather than on its face — with + * {@code REENTRY_DEPTH > CARRY_MARGIN}, so coming back costs + * {@code REENTRY_DEPTH + CARRY_MARGIN} of deliberate travel and cannot happen by drift.

    + * + *

    Both are FRACTIONS of the cell, never absolutes

    + * + *

    An absolute margin silently encodes an assumed speed and an assumed cell size: change either and + * a number chosen for "about two seconds" quietly becomes two minutes or two ticks. Derived from + * {@link GalacticCoord#HALF_CELL} they move with the cell, and the property they were chosen for — + * a duration — survives.

    + */ +public final class CellSeam { + + /** + * How far past its cell's face a pose must be before the ship is carried: {@code HALF_CELL/10 000} + * = 1 600 blocks at today's cell. Ratified 2026-08-17 in flight time, which is the unit that + * matters: about 2 s at a 40 b/t cruise, and still 4 ticks for a craft doing 395 b/t (first cosmic + * velocity, which the acceleration law makes reachable). Small enough that the ship is never long + * in a place its cell does not name, large enough that no single tick of any plausible speed + * straddles the decision. + */ + public static final long CARRY_MARGIN = GalacticCoord.HALF_CELL / 10_000L; + + /** + * How far inside the neighbour's opposite face the carried ship is placed: {@code HALF_CELL/1 000} + * = 16 000 blocks, ten times {@link #CARRY_MARGIN}. Ratified 2026-08-17: coming straight back is + * about 20 s of deliberate flight at a 40 b/t cruise, so a pilot who crosses knows he crossed. + */ + public static final long REENTRY_DEPTH = GalacticCoord.HALF_CELL / 1_000L; + + private CellSeam() { } + + /** + * The local offset a world-frame pose component maps to, per {@link CellWorldMapper}'s honest-3D + * mapping. Y carries the pose band; X and Z do not. + */ + public static long localOf(double world, boolean isY) { + long rounded = Math.round(world); + return isY ? rounded - GalacticCoord.HALF_CELL - CellWorldMapper.POSE_BAND_Y : rounded; + } + + /** + * Whether a pose has left its cell far enough to be CARRIED rather than merely reported at the + * boundary. Strictly more than the margin past the face, on any one axis. + * + *

    Deliberately not the same question as {@link CellWorldMapper#poseEscapesCell}: that one asks + * whether the REPORT had to saturate, and it is true the moment a pose steps a single block out — + * including the arrival paste band, which sits far below the cell's own pose range for the few + * ticks between the paste and the settle. A carry keyed on that question would fire on every + * arrival.

    + */ + public static boolean shouldCarry(double wx, double wy, double wz) { + return beyondMargin(localOf(wx, false)) + || beyondMargin(localOf(wy, true)) + || beyondMargin(localOf(wz, false)); + } + + private static boolean beyondMargin(long local) { + return local > GalacticCoord.HALF_CELL + CARRY_MARGIN + || local < -GalacticCoord.HALF_CELL - CARRY_MARGIN; + } + + /** + * Where the ship belongs after being carried out of {@code cell} by {@code pose}: the neighbouring + * cell it left through, with the ship set {@link #REENTRY_DEPTH} inside the face it came in by. + * + *

    Only the axes that actually crossed move to the entry face. An axis that did not cross keeps + * the position the pilot flew it to (clamped into the local range, since a pose may sit a little + * outside without having crossed) — a ship leaving through the +X face has not consented to being + * re-centred in Y and Z.

    + */ + public static GalacticCoord carriedCoord(GalacticCoord cell, double wx, double wy, double wz) { + long lx = localOf(wx, false); + long ly = localOf(wy, true); + long lz = localOf(wz, false); + return GalacticCoord.ofSectorLocal( + cell.sectorX() + step(lx), cell.sectorY() + step(ly), cell.sectorZ() + step(lz), + placed(lx), placed(ly), placed(lz)); + } + + /** Which neighbour an axis left through: -1, 0 or +1 cell. */ + private static long step(long local) { + if (local > GalacticCoord.HALF_CELL + CARRY_MARGIN) { + return 1L; + } + return local < -GalacticCoord.HALF_CELL - CARRY_MARGIN ? -1L : 0L; + } + + /** The local offset inside the destination cell for one axis. */ + private static long placed(long local) { + long crossed = step(local); + if (crossed > 0L) { + // Left through the +face: arrive just inside the neighbour's -face. + return -GalacticCoord.HALF_CELL + REENTRY_DEPTH; + } + if (crossed < 0L) { + return GalacticCoord.HALF_CELL - REENTRY_DEPTH; + } + return Math.max(-GalacticCoord.HALF_CELL, Math.min(GalacticCoord.HALF_CELL - 1L, local)); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/space/CellSeamController.java b/src/main/java/zmaster587/advancedRocketry/space/CellSeamController.java new file mode 100644 index 000000000..3b512e3f1 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/space/CellSeamController.java @@ -0,0 +1,186 @@ +package zmaster587.advancedRocketry.space; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.LongSupplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import net.minecraft.util.math.BlockPos; + +/** + * Carries a ship that has flown out of its cell into the neighbouring cell it left through. + * + *

    Before this existed, a ship past its cell face was neither stopped nor carried: its pose kept + * going while the ledger report SATURATED at the boundary, so the ship was in one place and named in + * another. Everything keyed on the name then answered about the wrong cell — it could not descend + * (the named cell holds no bodies), its jumps were refused, and the cell it was really in lost the + * ledger's garbage-collection protection.

    + * + *

    The arithmetic — when a pose counts as having left, and where in the neighbour the ship belongs + * — is {@link CellSeam}'s, and has no Minecraft in it. What lives here is the world half: acquiring + * the destination cell, capturing the crew, driving the shared {@link ShipCrossingService}, and the + * refcount handoff.

    + * + *

    The handoff order, and why it is not the other one

    + * + *

    The destination is materialized before the source is released. The reverse order leaves a + * window in which the ship holds no cell at all, and a garbage collection landing in that window + * collects the very cell the ship is being pasted into. The cost of this order is that a refused + * carry must hand the destination back, which is what the failure paths below do.

    + * + *

    A refusal is a normal outcome, not an error: the pool can be full. A refused ship keeps flying + * with its report saturated at the boundary — the old behaviour, now the fallback rather than the + * rule — and the carry is retried after a cooldown.

    + */ +public final class CellSeamController { + + private static final Logger LOGGER = LogManager.getLogger("advancedrocketry/space"); + + /** Ticks before a refused carry may be attempted again. */ + private static final int RETRY_COOLDOWN_TICKS = 100; + + /** The arrival paste band in the destination slot world — the entry crossing's geometry, because + * it is the same kind of destination: an empty slot world with nothing at its origin. */ + private static final int SEAM_PASTE_Z = -1024; + private static final int SEAM_PASTE_Y = 200; + private static final int SEAM_LANE_STRIDE = 64; + private static final int SEAM_LANE_COUNT = 8; + + private final SpaceManager space; + private final ShipLedger ledger; + private final ShipCrossingService crossing; + private final LongSupplier clock; + private final Map retryAfter = new HashMap<>(); + private int laneCounter; + + public CellSeamController(SpaceManager space, ShipLedger ledger, ShipCrossingService.Ops ops, + LongSupplier clock) { + this.space = space; + this.ledger = ledger; + this.crossing = new ShipCrossingService(ops); + this.clock = clock; + } + + /** + * Carry the SETTLED ship at {@code afcPos} out of {@code cell} and into the neighbour its pose has + * left through. Returns {@code true} when the crossing started, in which case the ship has been + * cut out of this world and the caller must stop touching it this tick. + * + *

    {@code shipPos} is passed in rather than re-read: the decision and the arrival must be + * computed from the SAME pose. Re-reading it here would let a fast ship be judged on one position + * and placed by another, and at these speeds the two can be thousands of blocks apart.

    + */ + public boolean requestCarry(int slotDim, BlockPos afcPos, UUID shipId, GalacticCoord cell, + double[] shipPos) { + if (shipId == null || cell == null || shipPos == null || crossing.isCrossing(shipId)) { + return false; + } + ShipLedger.Entry entry = ledger.get(shipId); + if (entry == null || entry.state != ShipLedger.State.SETTLED) { + // Only a ship genuinely settled in a cell can leave one by flying. A ship mid-arrival sits + // in the paste band, which is far outside its cell's pose range and would otherwise read as + // an escape on every single crossing. + return false; + } + if (!CellSeam.shouldCarry(shipPos[0], shipPos[1], shipPos[2])) { + return false; + } + long now = clock.getAsLong(); + Long cooldown = retryAfter.get(shipId); + if (cooldown != null && now < cooldown) { + return false; + } + + final GalacticCoord sourceCell = entry.coord; + final GalacticCoord destCoord = CellSeam.carriedCoord(cell, shipPos[0], shipPos[1], shipPos[2]); + + final int destSlotDim; + try { + destSlotDim = space.materialize(destCoord); + } catch (SpaceManager.PoolExhaustedException full) { + // No slot for the neighbour. The ship stays where it is, keeps flying, and keeps reporting + // saturated at the face — wrong by the overshoot, but pointing at a cell that exists. The + // crew is only READ here, so nobody is dismounted by a refusal. + List told = crossing.ops().peekCrew(slotDim, afcPos, shipPos); + LOGGER.warn("[SPACE] cell-seam carry refused for ship {} leaving {}: {} (told {} aboard)", + shipId, sourceCell.cellKey(), full.getMessage(), told == null ? 0 : told.size()); + crossing.ops().messageCrew(told, "msg.shipseam.refused"); + retryAfter.put(shipId, now + RETRY_COOLDOWN_TICKS); + return false; + } + + // Capture only now, with the destination GRANTED — the last refusal is behind — and still + // before the cut: the crossing cuts the seat blocks, and a post-cut capture finds nothing. + final List crew = crossing.ops().captureCrew(slotDim, afcPos, shipPos); + + int lane = (laneCounter++ % SEAM_LANE_COUNT); + double[] pose = CellWorldMapper.poseWorldOf(destCoord); + BlockPos anchor = crossing.begin(shipId, slotDim, shipPos, destSlotDim, + lane * SEAM_LANE_STRIDE, SEAM_PASTE_Y, SEAM_PASTE_Z, crew, pose, + new ShipCrossingService.Completion() { + @Override + public void settled(UUID id) { + ledger.settle(id, destCoord); + crossing.ops().messageCrew(crew, "msg.shipseam.arrived"); + LOGGER.info("[SPACE] cell-seam carry settled: ship {} now in cell {} (slot {})", + id, destCoord.cellKey(), destSlotDim); + } + + @Override + public void abandoned(UUID id) { + // The arrival never finished. The ship is somewhere in the destination slot + // world — which place depends on the half that stalled, and the crossing's own + // give-up line names it; do not claim one here. Settle it in the destination + // anyway: that IS the cell it is in, and leaving the row IN_TRANSIT would strand + // a real ship in a state nothing else advances. + ledger.settle(id, destCoord); + crossing.ops().messageCrew(crew, "msg.shipseam.failed"); + LOGGER.error("[SPACE] cell-seam settle never completed for ship {} arriving in " + + "cell {} (slot {}) - see the crossing give-up line above for which " + + "half stalled", id, destCoord.cellKey(), destSlotDim); + } + }); + if (anchor == null) { + LOGGER.error("[SPACE] cell-seam crossing failed for ship {} leaving cell {}", + shipId, sourceCell.cellKey()); + // The cut never produced a paste, so the ship is (best-effort) still intact where it was: + // hand the destination back, re-seat the crew we already captured, and let it keep flying. + space.dematerialize(destCoord); + crossing.ops().reseat(slotDim, + new BlockPos(shipPos[0], shipPos[1], shipPos[2]), crew, shipId, null); + crossing.ops().messageCrew(crew, "msg.shipseam.failed"); + retryAfter.put(shipId, now + RETRY_COOLDOWN_TICKS); + return false; + } + + // The ship is physically out of the source cell now, so the source is released NOW and not on + // settle — the settle only completes the arrival on the far side. The destination refcount was + // taken above, so the ship is never between cells. + space.markDirty(sourceCell); + space.dematerialize(sourceCell); + space.markDirty(destCoord); + // SETTLED at the destination, from the cut — deliberately NOT `beginTransit`. IN_TRANSIT is + // not a generic "crossing" state: `LoginRestore` reads it as "parked in the shared hyperspace + // world" and resolves the player through the transit dim, so a seam-crossing ship wearing it + // would orphan anyone who logged in during the few ticks of re-assembly. The row names the + // cell the ship's blocks are actually in, which is also the cell whose refcount is held. + ledger.settle(shipId, destCoord); + LOGGER.info("[SPACE] cell-seam carry started: ship {} {} -> {} (slot {})", + shipId, sourceCell.cellKey(), destCoord.cellKey(), destSlotDim); + return true; + } + + /** Advance every in-flight seam carry one tick (the shared crossing settle loop). */ + public void tick() { + crossing.tick(); + } + + /** Whether {@code shipId} is being carried across a cell face right now. */ + public boolean isCarrying(UUID shipId) { + return crossing.isCrossing(shipId); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystem.java b/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystem.java index 6da97d75f..acc8916ea 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystem.java +++ b/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystem.java @@ -67,6 +67,7 @@ public final class SpaceSubsystem { public final ShipTransitManager transit; public final ShipEntryController entry; public final DescentController descent; + public final CellSeamController seam; private int gcTickCounter; /** Set by the pool-pressure eviction listener; consumed on the next server tick to run an extra GC. */ private boolean pressureGcRequested; @@ -117,6 +118,8 @@ public SpaceSubsystem(SlotBinder binder, java.util.function.LongSupplier clock, SpaceSubsystem::launchBodyAddress, useClock); this.descent = new DescentController(this.manager, this.ledger, new VSShipCrossingOps(), new VSDescentPasteResolver(), useClock); + this.seam = new CellSeamController(this.manager, this.ledger, new VSShipCrossingOps(), + useClock); } /** The live subsystem, or {@code null} when none is attached (before server start, or on a client). */ @@ -209,6 +212,11 @@ public static ShipEntryController entry() { return current == null ? null : current.entry; } + /** The live cell-seam controller, or {@code null} when no subsystem is attached. */ + public static CellSeamController seam() { + return current == null ? null : current.seam; + } + /** The live descent controller, or {@code null} when no subsystem is attached. */ public static DescentController descent() { return current == null ? null : current.descent; diff --git a/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystemEvents.java b/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystemEvents.java index 7c569d745..6364fa05f 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystemEvents.java +++ b/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystemEvents.java @@ -72,6 +72,8 @@ public void onServerTick(TickEvent.ServerTickEvent event) { live.entry.tick(); // Advance in-flight DESCENTS (the inverse crossing, same async re-seat + settle). live.descent.tick(); + // Advance in-flight CELL-SEAM carries (a ship that flew out of its cell into the next one). + live.seam.tick(); // Rebroadcast the per-slot render bodies (throttled) so the slot-world sky (BoundarySky) // tracks each settled ship's direction to the bodies of its cell. SystemBodiesProducer.onBroadcastTick(FMLCommonHandler.instance().getMinecraftServerInstance()); diff --git a/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java b/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java index bba1847b1..d2dee1d72 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java @@ -498,10 +498,24 @@ public void update() { if (ledger != null && cell != null) { double[] pose = VSIntegration.getShipWorldPosition(world, getPos()); if (pose != null) { - // A ship reports its position WITHIN its cell. It may not rename the cell by moving: - // the name is the world it is in, the slot it is bound to and the ledger row that - // protects that cell from collection, and none of those follow a pose over a cell - // face. A pose outside the local range is therefore saturated, not carried. + // FLYING OUT OF THE CELL. A ship far enough past its face is carried into the + // neighbour it left through - the crossing cuts this tile out of the world, so + // nothing below may run on this tick. The margin that "far enough" means, and the + // depth the ship arrives at, are the seam's; this call site only owns the ORDER: + // the carry is asked BEFORE the position is reported, because a report that + // saturates is what a ship gets when the carry was refused, not what it gets while + // one is available. + zmaster587.advancedRocketry.space.CellSeamController seamCtl = + zmaster587.advancedRocketry.space.SpaceSubsystem.seam(); + if (seamCtl != null && seamCtl.requestCarry(world.provider.getDimension(), + getPos(), shipId, cell, pose)) { + return; + } + // The carry did not happen (none was needed, or the pool refused one). A ship + // reports its position WITHIN its cell: the name is the world it is in, the slot it + // is bound to and the ledger row that protects that cell from collection, and none + // of those follow a pose over a cell face on their own. So a pose outside the local + // range is saturated - wrong by the overshoot, but naming a cell that exists. ledger.updatePosition(shipId, zmaster587.advancedRocketry.space.CellWorldMapper .coordOfPoseWithin(cell, pose[0], pose[1], pose[2])); // Only a SETTLED ship can be at its cell's edge by flying there. A ship mid-crossing @@ -517,9 +531,10 @@ public void update() { .poseEscapesCell(pose[0], pose[1], pose[2])) { cellEdgeReported = true; zmaster587.advancedRocketry.AdvancedRocketry.logger.warn( - "[SPACE] ship {} reached the edge of cell {} (pose {},{},{}) - its position " - + "is held at the boundary. Leaving a neighbourhood is a jump, not a " - + "flight.", + "[SPACE] ship {} is outside cell {} (pose {},{},{}) and was not carried " + + "into the neighbour - its position is held at the boundary. " + + "Either it has not yet passed the carry margin, or the carry was " + + "refused (no free slot); the seam logs a refusal when it is one.", shipId, cellKey, pose[0], pose[1], pose[2]); } } diff --git a/src/main/resources/assets/advancedrocketry/lang/en_US.lang b/src/main/resources/assets/advancedrocketry/lang/en_US.lang index 3897e2a71..0d8b18901 100644 --- a/src/main/resources/assets/advancedrocketry/lang/en_US.lang +++ b/src/main/resources/assets/advancedrocketry/lang/en_US.lang @@ -915,6 +915,9 @@ msg.loginrestore.shipunknown=§cYour ship could not be found - the server has no msg.shipdescent.refused=§cThe descent could not start - the destination is not ready yet. Wait a moment and try again. msg.shipdescent.failed=§cThe descent failed - the ship could not cross onto the planet. msg.shipdescent.arrived=§aDescent complete - the ship is in the sky over the planet. Take her down. +msg.shipseam.refused=§cThe ship has left this neighbourhood, but space is saturated - no room to carry her across. Turn back. +msg.shipseam.failed=§cThe crossing failed - the ship could not be carried into the next neighbourhood. +msg.shipseam.arrived=§aThe ship has crossed into the next neighbourhood. msg.shiptransit.departed=§aJump engaged - the ship is under way. Helm control is offline until you arrive. msg.shiptransit.arrived=§aArrived - the ship is back in normal space. Helm control is yours again. msg.shiptransit.arrivalrecovered=§eThe jump completed, but not cleanly - your ship is at its arrival point rather than on course. It is safe; report this. diff --git a/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang b/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang index 506099b21..9fda3f153 100644 --- a/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang +++ b/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang @@ -516,6 +516,9 @@ msg.loginrestore.shipunknown=§cВаш корабль не найден — на msg.shipdescent.refused=§cСнижение не началось — точка прибытия ещё не готова. Подождите немного и попробуйте снова. msg.shipdescent.failed=§cСпуск не удался — корабль не смог перейти на планету. msg.shipdescent.arrived=§aСнижение выполнено — корабль в небе над планетой. Ведите его вниз. +msg.shipseam.refused=§cКорабль покинул эту окрестность, но космос перегружен — перенести его некуда. Поворачивайте назад. +msg.shipseam.failed=§cПереход не удался — корабль не смог перейти в соседнюю окрестность. +msg.shipseam.arrived=§aКорабль перешёл в соседнюю окрестность. msg.shiptransit.departed=§aПрыжок начат — корабль в пути. Управление отключено до прибытия. msg.shiptransit.arrived=§aПрибытие — корабль снова в обычном пространстве. Управление снова ваше. msg.shiptransit.arrivalrecovered=§eПрыжок завершён, но не чисто — корабль стоит в точке прибытия, а не на курсе. Он цел; сообщите об этом. diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCellSeamE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCellSeamE2ETest.java new file mode 100644 index 000000000..a41516799 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCellSeamE2ETest.java @@ -0,0 +1,334 @@ +package zmaster587.advancedRocketry.test.server; + +import com.github.stannismod.forge.testing.TestTimeouts; + +import zmaster587.advancedRocketry.space.CellSeam; +import zmaster587.advancedRocketry.space.GalacticCoord; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Test; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * E2E: a ship flown out through its cell's face ARRIVES IN THE NEIGHBOUR, and its ledger row names the + * cell it is actually in. + * + *

    Before the seam existed, such a ship was neither stopped nor carried: the pose kept going while + * the ledger report saturated at the boundary, leaving the ship in one place and named in another — + * unable to descend, refused its jumps, and no longer protecting the cell it was really in.

    + * + *

    The arrangement uses the REAL on-ramp to get a ship legitimately settled in a cell (assemble, + * hold a throttle, climb past the ceiling, let the flight computer's own tick call entry), then moves + * it past the face and drives {@code SpaceSubsystem.seam().requestCarry()} — production code, through + * a probe verb. The crossing itself is the shared one every other crossing uses.

    + * + *

    What this test does NOT cover, stated rather than implied: the trigger wiring inside + * {@code TileAdvancedFlightComputer}. A headless slot world has no player and no ticking chunks, so + * its tiles do not tick and no e2e here can observe that call — the same limit the descent e2e has + * (it drives {@code space descent-begin} and says so). WHEN a carry fires is pinned deterministically + * by {@code CellSeamTest}; the one link neither covers is the two lines in the tile that join them.

    + * + *

    Witnesses, in order: the ledger names the +X neighbour and no other cell; the ship arrives + * {@code REENTRY_DEPTH} INSIDE that neighbour's opposite face rather than on it; and it stays there — + * a ship placed on the face would be one drift away from crossing straight back, which is the whole + * content of the hysteresis. CONTROL: the pre-move ledger read is asserted to name the source cell, so + * the later change is a real observation rather than a first reading.

    + * + *

    Gated on the server's real VS presence (run with {@code -PwithVS}); skips cleanly otherwise.

    + */ +public class VSShipCellSeamE2ETest extends AbstractSharedServerTest { + + private static final Pattern BUILDER_POS = + Pattern.compile("\"builderPos\":\\[(-?\\d+),(-?\\d+),(-?\\d+)]"); + private static final Pattern CELL_KEY = Pattern.compile("^(-?\\d+)_(-?\\d+)_(-?\\d+)$"); + + /** Where this test builds its ship — its own region, clear of the entry/descent legs. */ + private static final int SRC_X = 6800, SRC_Y = 80, SRC_Z = 6800; + /** A world Y comfortably above the default orbit ceiling (ARConfiguration.orbit = 1000). */ + private static final int ABOVE_CEILING_Y = 1200; + + /** Async settle budget, stretched by the build's fork factor (the entry leg's sizing). */ + private static final int SETTLE_POLLS = (int) Math.ceil(120 * TestTimeouts.factor()); + + /** + * How far past the face the ship is placed: comfortably beyond the carry margin, so the test is + * not sitting on the decision boundary — that is {@code CellSeamTest}'s job, on the pure layer, + * where a one-block question can be asked without a physics engine in the way. + */ + private static final long PAST_THE_FACE = + GalacticCoord.HALF_CELL + CellSeam.CARRY_MARGIN + 2_000L; + + /** + * Tolerance on the arrival position. The contract is "inside the face, not on it", and the two + * candidates are {@code REENTRY_DEPTH} apart (16 000 blocks), so a few hundred blocks of settle + * slop cannot confuse them. + */ + private static final double ARRIVAL_TOLERANCE = 2_000d; + + @Test + public void aShipFlownPastItsCellFaceIsCarriedIntoTheNeighbourAndStaysThere() throws Exception { + Assume.assumeTrue("needs Valkyrien Skies on the server classpath (run with -PwithVS)", + serverHasVs()); + + exec("artest vs permaload true"); + String setup = exec("artest space entry-setup 2"); + assertTrue("entry setup failed: " + setup, setup.contains("\"ok\":true")); + + // CONTROL: nothing is ledgered yet, so a later reading is a real observation — and the + // "first ledgered ship" this test reads its durable id from is unambiguously ours. + String before = exec("artest space entry-status"); + assertEquals("no ship must be ledgered before the climb: " + before, 0, + extractInt(before, "ships")); + + // --- Arrangement: get a ship into a cell through the production on-ramp ------------------ + clearArea(SRC_X, SRC_Z); + String coords = placeFixture(SRC_X, SRC_Y, SRC_Z, "with-pilot-seat"); + String asm = exec("artest rocket assemble 0 " + coords); + assertTrue("with VS an AFC-bearing build must route to a ship (no rocket): " + asm, + asm.contains("\"rocketCount\":0")); + assertTrue("the source VS ship never loaded", waitForLoadedShip(0) >= 1); + + // TWO IDENTITIES, deliberately kept apart. `vs ship-info` answers the VS ship uuid + // (`VSBridge.nearestShipId` -> `getShipData().getUuid()`), which every `vs` verb takes and + // which a crossing REPLACES — the arriving ship is a new VS body. The ledger is keyed by AR's + // durable ship id, read from `entry-status` once the ship is in space. Asking either side with + // the other's id answers "not found" and reads exactly like the mechanic being broken. + String srcInfo = exec("artest vs ship-info 0 " + SRC_X + " " + SRC_Y + " " + SRC_Z); + assertTrue("source ship not managed by VS: " + srcInfo, srcInfo.contains("\"managed\":true")); + String srcVsId = extractString(srcInfo, "id"); + assertTrue("the assembled ship reported no VS id: " + srcInfo, srcVsId != null); + double sx = extractDouble(srcInfo, "posX"), sy = extractDouble(srcInfo, "posY"), + sz = extractDouble(srcInfo, "posZ"); + + String heldInput = exec("artest vs ff-input-by-id 0 " + srcVsId + " 0 1 0 0 0 0"); + assertTrue("the held input must reach this ship's flight computer: " + heldInput, + heldInput.contains("\"afcResolved\":true")); + assertTrue("climb teleport failed", + exec("artest vs teleport-ship 0 " + (int) sx + " " + (int) sy + " " + (int) sz + + " " + (int) sx + " " + ABOVE_CEILING_Y + " " + (int) sz) + .contains("\"ok\":true")); + exec("artest vs unpark 0 " + (int) sx + " " + ABOVE_CEILING_Y + " " + (int) sz); + + String status = ""; + String sourceCell = null; + for (int i = 0; i < SETTLE_POLLS; i++) { + status = exec("artest space entry-status"); + if (extractInt(status, "ships") >= 1 && "SETTLED".equals(extractString(status, "state"))) { + sourceCell = extractString(status, "cellKey"); + break; + } + loadAllEntrySlots(setup); + Thread.sleep(250); + } + assertTrue("the ship never reached space through the entry path; last status=" + status, + sourceCell != null); + String arShipId = extractString(status, "shipId"); + assertTrue("the settled ship has no durable id: " + status, arShipId != null); + int sourceSlot = extractInt(status, "slotDim"); + assertTrue("settled ship has no bound slot: " + status, sourceSlot > Integer.MIN_VALUE); + assertTrue("the settled ship's cell world is not live", waitForLoadedShip(sourceSlot) >= 1); + + // --- CONTROL: while it is inside its cell, the ledger names THAT cell -------------------- + String inside = exec("artest space ledger-get " + arShipId); + assertTrue("the ledger does not know the settled ship: " + inside, + inside.contains("\"found\":true")); + assertEquals("the ledger must name the source cell before the ship leaves it: " + inside, + sourceCell, extractString(inside, "cell")); + + // --- Act: put the ship past the +X face of its cell -------------------------------------- + String inCell = shipInThatSlot(sourceSlot); + double cx = extractDouble(inCell, "posX"), cy = extractDouble(inCell, "posY"), + cz = extractDouble(inCell, "posZ"); + assertFalse("the ship's in-cell pose could not be read: " + inCell, + Double.isNaN(cx) || Double.isNaN(cy) || Double.isNaN(cz)); + + String outward = exec("artest vs teleport-ship " + sourceSlot + " " + + (long) cx + " " + (long) cy + " " + (long) cz + " " + + PAST_THE_FACE + " " + (long) cy + " " + (long) cz); + assertTrue("the move past the cell face failed: " + outward, outward.contains("\"ok\":true")); + exec("artest vs unpark " + sourceSlot + " " + PAST_THE_FACE + " " + (long) cy + " " + (long) cz); + + // THE ARRANGEMENT IS ASSERTED, not assumed. "the probe returned ok" is not "the ship is past + // the face": a clamp, a refused transform or a Y-limit would all report ok and leave the ship + // inside its cell, and the carry would then be correctly not firing — a green mechanic + // reported as a red one. + String moved = shipInThatSlot(sourceSlot); + double mx = extractDouble(moved, "posX"); + assertFalse("the moved ship's pose could not be read: " + moved, Double.isNaN(mx)); + assertTrue("the ship is not actually past the cell face after the move — it is at x=" + mx + + ", and the carry threshold is " + (GalacticCoord.HALF_CELL + + CellSeam.CARRY_MARGIN) + "; the test moved nothing: " + moved, + mx > GalacticCoord.HALF_CELL + CellSeam.CARRY_MARGIN); + + // Drive the production carry. NOT the flight computer's own tick: a headless slot world has + // no player and no ticking chunks, so its tiles do not tick — an earlier revision of this test + // waited 30 s for a trigger that cannot fire here and reported the CROSSING as broken. This is + // the same split the descent e2e already uses (`space descent-begin`): WHEN a carry fires is + // pinned deterministically by `CellSeamTest`, and what a carry DOES is pinned here, on a real + // ship. `wouldCarry` is production's own reading of the live pose, so the arrangement is + // witnessed by the code under test rather than only by this test's arithmetic. + String carry = exec("artest space seam-carry " + sourceSlot); + assertTrue("production does not agree the ship has left its cell (its own predicate on the " + + "live pose): " + carry, carry.contains("\"wouldCarry\":true")); + assertTrue("the carry did not start — the reason is in the reply: " + carry, + carry.contains("\"started\":true")); + + // --- Assert: carried into the neighbour --------------------------------------------------- + String carriedCell = null; + String afterMove = ""; + for (int i = 0; i < SETTLE_POLLS; i++) { + afterMove = exec("artest space ledger-get " + arShipId); + String cell = extractString(afterMove, "cell"); + if (cell != null && !sourceCell.equals(cell) + && "SETTLED".equals(extractString(afterMove, "state"))) { + carriedCell = cell; + break; + } + loadAllEntrySlots(setup); + Thread.sleep(250); + } + assertTrue("the carry started but the ship never settled in the neighbour; source=" + + sourceCell + " shipX=" + mx + " carry=" + carry + " last ledger=" + afterMove, + carriedCell != null); + + long[] from = cellSectors(sourceCell); + long[] to = cellSectors(carriedCell); + assertEquals("carried into the +X neighbour and no other", from[0] + 1L, to[0]); + assertEquals("the Y sector must not move — the ship crossed one face", from[1], to[1]); + assertEquals("the Z sector must not move — the ship crossed one face", from[2], to[2]); + + // It arrived INSIDE the neighbour's opposite face, not on it. This is the hysteresis as the + // world sees it: the expected world X is the local offset itself (XZ realize directly). + int carriedSlot = extractInt(afterMove, "slotDim"); + assertTrue("the carried ship has no bound slot: " + afterMove, carriedSlot > Integer.MIN_VALUE); + assertTrue("the neighbour's cell world never came up", waitForLoadedShip(carriedSlot) >= 1); + String arrived = shipInThatSlot(carriedSlot); + double ax = extractDouble(arrived, "posX"); + assertFalse("the arrived ship's pose could not be read: " + arrived, Double.isNaN(ax)); + double expectedX = -(double) GalacticCoord.HALF_CELL + CellSeam.REENTRY_DEPTH; + assertEquals("the ship must arrive the re-entry depth inside the face it came in by, not on it", + expectedX, ax, ARRIVAL_TOLERANCE); + + // And it STAYS there: a ship parked on the face would cross straight back. + for (int i = 0; i < 8; i++) { + Thread.sleep(250); + String held = exec("artest space ledger-get " + arShipId); + assertEquals("the carried ship bounced back across the face (ping-pong): " + held, + carriedCell, extractString(held, "cell")); + } + } + + @After + public void cleanup() throws Exception { + if (serverHasVs()) { + exec("artest space entry-clear"); + exec("artest vs permaload false"); + } + } + + /** The three sector indices of a {@code sx_sy_sz} cell key. */ + private static long[] cellSectors(String cellKey) { + Matcher m = CELL_KEY.matcher(cellKey); + assertTrue("unparsable cell key: " + cellKey, m.matches()); + return new long[]{Long.parseLong(m.group(1)), Long.parseLong(m.group(2)), + Long.parseLong(m.group(3))}; + } + + private String exec(String cmd) throws Exception { + return String.join("\n", client().execute(cmd)); + } + + /** + * The one ship in a cell's slot world, asked for POSITIONALLY and guarded by a count. + * + *

    A ship cannot be followed across a crossing by VS id — the arriving body is a new one — and + * the cell frame is far from any origin a query could guess, so the lookup is "nearest to the + * cell centre pose, within the whole cell". That is only an identity because the slot world holds + * exactly ONE ship, which is asserted here rather than assumed: without the count this returns a + * neighbour the moment a second ship shares the slot, and it reads identically.

    + */ + private String shipInThatSlot(int slotDim) throws Exception { + String count = exec("artest vs ship-count " + slotDim); + assertEquals("this lookup is only an identity while the slot holds exactly one ship: " + count, + 1, extractInt(count, "count")); + long centreY = GalacticCoord.HALF_CELL + 256L; // the cell-centre pose (CellWorldMapper band) + String info = exec("artest vs ship-info " + slotDim + " 0 " + centreY + " 0 " + + (GalacticCoord.CELL * 2L)); + assertTrue("the ship in slot " + slotDim + " could not be located: " + info, + info.contains("\"managed\":true")); + return info; + } + + private boolean serverHasVs() throws Exception { + return exec("artest vs available").contains("\"available\":true"); + } + + private void loadAllEntrySlots(String setup) throws Exception { + Matcher m = Pattern.compile("\"dims\":\\[(-?\\d+),(-?\\d+)]").matcher(setup); + if (m.find()) { + exec("artest vs load-ships " + m.group(1)); + exec("artest vs load-ships " + m.group(2)); + } + } + + private int waitForLoadedShip(int dim) throws Exception { + for (int i = 0; i < 40; i++) { + if (extractInt(exec("artest vs ship-count-all " + dim), "count") >= 1) { + exec("artest vs load-ships " + dim); + int loaded = extractInt(exec("artest vs ship-count " + dim), "count"); + if (loaded >= 1) { + return loaded; + } + } + Thread.sleep(250); + } + return 0; + } + + private void clearArea(int baseX, int baseZ) throws Exception { + int cx1 = (baseX - 4) >> 4, cz1 = (baseZ - 4) >> 4; + int cx2 = (baseX + 20) >> 4, cz2 = (baseZ + 20) >> 4; + assertTrue("chunk warmup failed", exec("artest chunk warmup 0 " + cx1 + " " + cz1 + " " + + cx2 + " " + cz2).contains("\"ok\":true")); + assertTrue("pre-clear failed", exec("artest fill 0 " + (baseX - 4) + " " + (SRC_Y - 2) + " " + + (baseZ - 4) + " " + (baseX + 20) + " " + (SRC_Y + 12) + " " + (baseZ + 20) + + " minecraft:air").contains("\"ok\":true")); + } + + private String placeFixture(int baseX, int baseY, int baseZ, String variant) throws Exception { + String fixture = exec("artest fixture rocket 0 " + baseX + " " + baseY + " " + baseZ + " " + variant); + assertTrue("fixture (" + variant + ") failed: " + fixture, fixture.contains("\"ok\":true")); + Matcher bp = BUILDER_POS.matcher(fixture); + assertTrue("fixture (" + variant + ") missing builderPos: " + fixture, bp.find()); + return bp.group(1) + " " + bp.group(2) + " " + bp.group(3); + } + + private static int extractInt(String json, String key) { + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+)").matcher(json); + return m.find() ? Integer.parseInt(m.group(1)) : Integer.MIN_VALUE; + } + + /** + * A number out of a probe reply, exponent form included — a coordinate past 10⁷ prints as + * {@code 1.6000256E7}, and an extractor that cannot read that silently compares 1.6 against + * sixteen million. NaN when absent, deliberately: the alternative (0.0) is a legal coordinate and + * would make a missing field read as "the ship is at the origin". + */ + private static double extractDouble(String json, String key) { + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?(?:[eE][-+]?\\d+)?)") + .matcher(json); + return m.find() ? Double.parseDouble(m.group(1)) : Double.NaN; + } + + private static String extractString(String json, String key) { + Matcher m = Pattern.compile("\"" + key + "\":\"([^\"]*)\"").matcher(json); + return m.find() ? m.group(1) : null; + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/CellSeamTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/CellSeamTest.java new file mode 100644 index 000000000..023d00795 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/CellSeamTest.java @@ -0,0 +1,131 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import zmaster587.advancedRocketry.space.CellSeam; +import zmaster587.advancedRocketry.space.CellWorldMapper; +import zmaster587.advancedRocketry.space.GalacticCoord; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for {@link CellSeam} — the arithmetic of flying THROUGH a cell face. + * + *

    What these pin is deliberately narrow: that a ship inside its cell is left alone, that one far + * enough past a face is carried into the neighbour it left through, that it arrives inside that + * neighbour rather than on its face, and that the return trip costs more than the outbound overshoot + * — which is the whole content of "no ping-pong". The margins themselves are read from the class, not + * restated, so a re-tuning changes the behaviour these tests describe without making them lie.

    + */ +public class CellSeamTest { + + private static final GalacticCoord CELL = GalacticCoord.ofSectorLocal(3L, -1L, 7L, 0, 0, 0); + + /** The world-frame pose whose local offset is {@code (lx,ly,lz)} — the inverse of the mapping. */ + private static double[] poseOfLocal(long lx, long ly, long lz) { + return new double[]{lx, ly + GalacticCoord.HALF_CELL + CellWorldMapper.POSE_BAND_Y, lz}; + } + + @Test + public void aShipInsideItsCellIsNotCarried() { + double[] deepInside = poseOfLocal(0L, 0L, 0L); + assertFalse(CellSeam.shouldCarry(deepInside[0], deepInside[1], deepInside[2])); + + // Right up against the face, and even a little past it: still not a crossing. This is the case + // the margin exists for — a report may saturate here, a ship may not change worlds here. + double[] onTheFace = poseOfLocal(GalacticCoord.HALF_CELL, 0L, 0L); + assertFalse(CellSeam.shouldCarry(onTheFace[0], onTheFace[1], onTheFace[2])); + double[] justPast = poseOfLocal(GalacticCoord.HALF_CELL + CellSeam.CARRY_MARGIN, 0L, 0L); + assertFalse(CellSeam.shouldCarry(justPast[0], justPast[1], justPast[2])); + } + + @Test + public void aShipPastTheMarginIsCarriedIntoTheNeighbourItLeftThrough() { + double[] out = poseOfLocal(GalacticCoord.HALF_CELL + CellSeam.CARRY_MARGIN + 1L, 0L, 0L); + assertTrue(CellSeam.shouldCarry(out[0], out[1], out[2])); + + GalacticCoord dest = CellSeam.carriedCoord(CELL, out[0], out[1], out[2]); + assertEquals("the +X neighbour, and only that one", CELL.sectorX() + 1L, dest.sectorX()); + assertEquals(CELL.sectorY(), dest.sectorY()); + assertEquals(CELL.sectorZ(), dest.sectorZ()); + assertEquals("placed inside the face it came in by", + -GalacticCoord.HALF_CELL + CellSeam.REENTRY_DEPTH, dest.localX()); + } + + @Test + public void theAxesThatDidNotCrossKeepWhereThePilotFlewThem() { + long ly = 4_242L; + long lz = -1_000_000L; + double[] out = poseOfLocal(-GalacticCoord.HALF_CELL - CellSeam.CARRY_MARGIN - 1L, ly, lz); + + GalacticCoord dest = CellSeam.carriedCoord(CELL, out[0], out[1], out[2]); + assertEquals(CELL.sectorX() - 1L, dest.sectorX()); + assertEquals("left through -X, so it arrives just inside the +X face", + GalacticCoord.HALF_CELL - CellSeam.REENTRY_DEPTH, dest.localX()); + assertEquals(ly, dest.localY()); + assertEquals(lz, dest.localZ()); + } + + @Test + public void aCornerExitCarriesEveryAxisThatCrossed() { + double[] out = poseOfLocal( + GalacticCoord.HALF_CELL + CellSeam.CARRY_MARGIN + 1L, + -GalacticCoord.HALF_CELL - CellSeam.CARRY_MARGIN - 1L, + GalacticCoord.HALF_CELL + CellSeam.CARRY_MARGIN + 1L); + + GalacticCoord dest = CellSeam.carriedCoord(CELL, out[0], out[1], out[2]); + assertEquals(CELL.sectorX() + 1L, dest.sectorX()); + assertEquals(CELL.sectorY() - 1L, dest.sectorY()); + assertEquals(CELL.sectorZ() + 1L, dest.sectorZ()); + } + + /** + * The hysteresis, measured on the ship rather than on the constants: from where a carry actually + * PUT it, flying straight back must cost at least {@code REENTRY_DEPTH + CARRY_MARGIN}. + * + *

    Every distance below is derived from the arrival coordinate. An earlier version of this test + * compared the two constants to each other and asserted about poses computed from them, and it + * stayed green against a build that landed the ship ON the face — which is the whole defect this + * test exists to catch, with the return trip cut by a factor of ten.

    + */ + @Test + public void aCarriedShipCannotPingPongBackAcrossTheFace() { + double[] out = poseOfLocal(GalacticCoord.HALF_CELL + CellSeam.CARRY_MARGIN + 1L, 0L, 0L); + GalacticCoord dest = CellSeam.carriedCoord(CELL, out[0], out[1], out[2]); + + double[] arrival = CellWorldMapper.poseWorldOf(dest); + assertFalse("the arrival pose must not itself be a crossing", + CellSeam.shouldCarry(arrival[0], arrival[1], arrival[2])); + + // Where it landed, and how far back the return threshold is FROM THERE. + long arrivedAt = CellSeam.localOf(arrival[0], false); + long returnThreshold = -GalacticCoord.HALF_CELL - CellSeam.CARRY_MARGIN; + long returnTrip = arrivedAt - returnThreshold; + assertTrue("returning must cost the re-entry depth plus the margin, not merely the margin: " + + "arrived at " + arrivedAt + ", threshold " + returnThreshold, + returnTrip >= CellSeam.REENTRY_DEPTH + CellSeam.CARRY_MARGIN); + + // And the threshold is where it says it is: one block short does not cross, one past does. + double[] almostBack = poseOfLocal(arrivedAt - returnTrip + 1L, 0L, 0L); + assertFalse("one block short of the return threshold is still not a crossing", + CellSeam.shouldCarry(almostBack[0], almostBack[1], almostBack[2])); + double[] allTheWayBack = poseOfLocal(arrivedAt - returnTrip - 1L, 0L, 0L); + assertTrue("one block past it must carry the ship back", + CellSeam.shouldCarry(allTheWayBack[0], allTheWayBack[1], allTheWayBack[2])); + } + + /** + * Both margins are fractions of the cell. Pinned because the failure they guard against is silent: + * an absolute margin keeps its number when the cell is resized and quietly becomes a different + * duration — which is exactly what happened to the moon band before it was expressed this way. + */ + @Test + public void theMarginsScaleWithTheCell() { + assertEquals(GalacticCoord.HALF_CELL / 10_000L, CellSeam.CARRY_MARGIN); + assertEquals(GalacticCoord.HALF_CELL / 1_000L, CellSeam.REENTRY_DEPTH); + assertTrue("the re-entry depth must exceed the carry margin, or the hysteresis is inverted", + CellSeam.REENTRY_DEPTH > CellSeam.CARRY_MARGIN); + } +} From 8896a61d454403ab0c72cc149b40653804721bb0 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 11:38:55 +0300 Subject: [PATCH 26/42] feat: an atmosphere charges for speed, and a held key keeps arriving - add atmosphericDrag: the bound is where you are, not the law - derive the drag constant from a stated terminal speed - re-assert a held pilot input, phased per seat, so a lost one costs 1s - make server wait admit it never advanced the clock - retarget the FA capture test at the contract that still exists --- .../api/FreeFlightPhysics.java | 62 +++++++++++ .../api/PilotInputCadence.java | 77 +++++++++++++ .../advancedRocketry/client/KeyBindings.java | 14 ++- .../command/test/TestProbeCommand.java | 11 ++ .../advancedRocketry/entity/EntityRocket.java | 19 ++++ ...ipEntryRefusedKeepsPilotSeatedE2ETest.java | 28 ++++- .../test/server/FreeFlightAssistsE2ETest.java | 53 ++++++++- .../ServerWaitProbeReportsRealTicksTest.java | 69 ++++++++++++ .../test/unit/AtmosphericDragTest.java | 97 +++++++++++++++++ .../test/unit/PilotInputCadenceTest.java | 103 ++++++++++++++++++ 10 files changed, 530 insertions(+), 3 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/api/PilotInputCadence.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/server/ServerWaitProbeReportsRealTicksTest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/AtmosphericDragTest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/PilotInputCadenceTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/api/FreeFlightPhysics.java b/src/main/java/zmaster587/advancedRocketry/api/FreeFlightPhysics.java index 6de6549a5..10ec3a09a 100644 --- a/src/main/java/zmaster587/advancedRocketry/api/FreeFlightPhysics.java +++ b/src/main/java/zmaster587/advancedRocketry/api/FreeFlightPhysics.java @@ -88,6 +88,34 @@ public final class FreeFlightPhysics { */ public static final double MAX_THRUST_ACCEL = 0.5; + /** + * The speed a craft at FULL thrust settles at in a one-atmosphere sky, in blocks/tick — the + * number {@link #DRAG_PER_DENSITY} is derived from, and the one to argue about if this ever + * feels wrong. + * + *

    100 b/t is 2 km/s. It is deliberately generous: real hulls come apart far below it in dense + * air, and the point here is not to model aerodynamics but to stop an atmosphere from being a + * thing a craft passes through as if it were vacuum. Under the acceleration law a rocket can now + * arrive at a planet arbitrarily fast, and nothing charged it for that; an atmosphere charges it + * in the only currency this law has, which is TIME — shedding speed takes as long as building it + * did.

    + * + *

    NOT ratified as a balance number. It is derived, stated, and pinned by a test that reads it + * from here rather than restating it.

    + */ + public static final double ATMOSPHERIC_TERMINAL_SPEED = 100.0; + + /** + * Quadratic drag per unit of atmospheric density, in 1/blocks: {@code Δv = -k·ρ·v·|v|}. + * + *

    Derived, not chosen: at terminal velocity thrust equals drag, so + * {@code k = MAX_THRUST_ACCEL / ATMOSPHERIC_TERMINAL_SPEED²}. Both inputs are visible above, so + * changing either moves this the way physics says it should rather than the way a hand-tuned + * constant would.

    + */ + public static final double DRAG_PER_DENSITY = + MAX_THRUST_ACCEL / (ATMOSPHERIC_TERMINAL_SPEED * ATMOSPHERIC_TERMINAL_SPEED); + /** * Per-tick velocity retention used by the liftoff/hover assist to bleed * horizontal drift (0..1; ≈0.88 → settles in ~25–30 ticks). @@ -480,6 +508,40 @@ public static Step translateNewtonian(double mx, double my, double mz, Quat q, return new Step(newMx, newMy, newMz, e[0], e[1], e[2], thrustApplied); } + /** + * One tick of atmospheric drag on a world-frame velocity: {@code Δv = -k·ρ·v·|v|}, quadratic in + * speed and linear in density, applied along the velocity vector so it only ever slows a craft + * and never turns it. + * + *

    Applies to every flight law rather than to one of them: an atmosphere does not ask whether + * Flight Assist is on. It is the counterpart of removing the speed cap — the cap used to be the + * only thing standing between "go as fast as you like" and "arrive at a planet at any speed", + * and a bound that comes from where you are is a better one than a bound written into the law.

    + * + *

    Never overshoots into a reversal. A tick's drag is clamped to the speed itself, so a + * craft can be brought to rest but never pushed backwards by air — which an unclamped quadratic + * would do at high speed and low tick rate, and which reads as a hull bouncing off the sky.

    + * + * @param density atmospheric density as a fraction of one Earth atmosphere; {@code <= 0} is + * vacuum and returns the velocity untouched + * @return the new {@code {mx, my, mz}} + */ + public static double[] atmosphericDrag(double mx, double my, double mz, double density) { + if (!(density > 0.0)) { + return new double[]{mx, my, mz}; + } + double speed = Math.sqrt(mx * mx + my * my + mz * mz); + if (speed < 1e-9) { + return new double[]{mx, my, mz}; + } + double decel = DRAG_PER_DENSITY * density * speed * speed; + if (decel > speed) { + decel = speed; // to rest, never through it + } + double scale = (speed - decel) / speed; + return new double[]{mx * scale, my * scale, mz * scale}; + } + // -- Tier-2 ship translation command ----------------------------------- /** diff --git a/src/main/java/zmaster587/advancedRocketry/api/PilotInputCadence.java b/src/main/java/zmaster587/advancedRocketry/api/PilotInputCadence.java new file mode 100644 index 000000000..e2ad87f1f --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/api/PilotInputCadence.java @@ -0,0 +1,77 @@ +package zmaster587.advancedRocketry.api; + +/** + * When a pilot's control packet must go out, given that the last one may not have survived. + * + *

    Why this is not simply "on change"

    + * + *

    Send-on-change is correct only if two things hold: delivery is lossless, and the value the server + * stored stays stored. The second does not hold. The server keeps a pilot's input in a field on the + * flight computer's TILE INSTANCE, and a tile instance is a perishable thing — a chunk reload or a + * re-registration replaces the object, and the field comes back null. The client, meanwhile, has + * nothing to notice: from where it sits the key is still down and the input has not changed, so under + * send-on-change it never speaks again and the craft flies on with no one at the controls.

    + * + *

    Measured as a symptom before it was understood: a held climb key lifted a ship for about 100 + * ticks and then the ship simply held altitude, with the key still down and the residual vertical + * velocity oscillating about zero — a craft being HELD, not one coasting. The probe path that + * re-sent its command every tick never showed it, which is the same fact from the other side.

    + * + *

    The rule

    + * + *

    A CHANGE is sent immediately, as before. A held non-idle input is re-sent every + * {@link #REPEAT_TICKS} ticks, so the cost of any single loss is bounded by that interval instead of + * lasting until the pilot happens to move a control. An IDLE input is never repeated: losing "no + * input" costs nothing, because the absence of input is what the server falls back to anyway.

    + * + *

    The phase is derived from the seat, not shared: a fixed {@code tick % N} would stack every pilot + * on a server into the same tick, which is how a keep-alive turns into a burst. Two seats therefore + * repeat on different ticks even when their pilots pressed at the same instant.

    + */ +public final class PilotInputCadence { + + /** + * How often a held input is re-asserted, in ticks — one second at 20 tps. + * + *

    Chosen in the units of the defect: this is the worst-case time a craft can fly with a + * command the server has forgotten. At 20 ticks the pilot may feel a stutter; the loss it + * replaces lasted until he released the key, which in the measured case was the rest of the + * flight. One packet per second per seated pilot is negligible beside the per-tick pose stream + * the same ship already sends.

    + */ + public static final int REPEAT_TICKS = 20; + + private PilotInputCadence() { } + + /** + * Whether this tick must put {@code input} on the wire. + * + * @param input what the pilot is commanding right now; {@code null} is never sent + * @param lastSent the last input actually sent, or {@code null} if none has been + * @param tick a monotonically increasing client tick counter + * @param seatPhase a per-seat phase offset (see the class doc); any stable integer derived from + * the seat's identity will do + */ + public static boolean shouldSend(FreeFlightInput input, FreeFlightInput lastSent, + long tick, int seatPhase) { + if (input == null) { + return false; + } + if (!input.equals(lastSent)) { + return true; + } + if (input.isIdle()) { + return false; + } + return Math.floorMod(tick - seatPhase, REPEAT_TICKS) == 0L; + } + + /** + * A stable phase in {@code [0, REPEAT_TICKS)} for a seat at {@code (x,y,z)}. Deliberately not a + * hash of the whole position object: two seats a block apart must land on different ticks, and + * the sum of the coordinates does exactly that while staying trivially reproducible in a test. + */ + public static int phaseOfSeat(int x, int y, int z) { + return Math.floorMod(x + y + z, REPEAT_TICKS); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/client/KeyBindings.java b/src/main/java/zmaster587/advancedRocketry/client/KeyBindings.java index ec8b593a7..f5b2882b2 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/KeyBindings.java +++ b/src/main/java/zmaster587/advancedRocketry/client/KeyBindings.java @@ -17,6 +17,7 @@ import zmaster587.advancedRocketry.api.Constants; import zmaster587.advancedRocketry.api.EntityRocketBase; import zmaster587.advancedRocketry.api.FreeFlightInput; +import zmaster587.advancedRocketry.api.PilotInputCadence; import zmaster587.advancedRocketry.api.FreeFlightPhysics; import zmaster587.advancedRocketry.api.RocketFlightMode; import zmaster587.advancedRocketry.command.test.TestProbeCommandRegistration; @@ -135,6 +136,11 @@ public static float flightCursorY(float partialTicks) { /** PACKET_PILOT_INPUT packets this client actually dispatched to the seat. */ public static volatile int shipInputSendCount; + /** Client ticks of ship control, the clock {@link PilotInputCadence} counts its repeat + * interval on. Not a world time: it must keep counting while the world's own clock is + * whatever a loading screen left it at. */ + private static long shipInputTick; + public static boolean isCameraPinnedThisFlight() { return cameraPinValid; } @@ -716,7 +722,13 @@ private boolean handleShipPilotInput(Minecraft mc, EntityPlayerSP player) { hudPitchRate = pitch; FreeFlightInput input = new FreeFlightInput(fwd, vert, strafe, yaw, pitch, roll, brake, cut); - if (!input.equals(lastSentShipInput)) { + // A change goes out at once; a HELD non-idle input is also re-asserted on its seat's own + // phase. The server keeps this input on a tile INSTANCE, and an instance does not outlive a + // chunk reload — so under send-on-change alone a craft flies on with a command the server has + // forgotten and a pilot who has no way to know. See PilotInputCadence for the measurement. + shipInputTick++; + if (PilotInputCadence.shouldSend(input, lastSentShipInput, shipInputTick, + PilotInputCadence.phaseOfSeat(seatPos.getX(), seatPos.getY(), seatPos.getZ()))) { seat.pendingInput = input; PacketHandler.sendToServer(new PacketMachine(seat, TilePilotSeat.PACKET_PILOT_INPUT)); shipInputSendCount++; diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 0735da4d3..7aa779658 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -20609,11 +20609,22 @@ private void handleServer(MinecraftServer server, ICommandSender sender, String[ catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } long end = world.getTotalWorldTime(); + // SAY whether the clock actually moved. Measured 2026-08-17: it does not — this handler + // runs ON the server thread, the one thread that advances world time, so the poll above + // can only ever watch a stopped clock and then give up on its wall budget. Every caller + // that read this as "N ticks have now happened" was reading a sleep. The verb keeps + // working (it does burn wall time, which some callers only ever wanted) but it may not + // report that silently: `advanced` is the field a test must look at, and the hint says + // what to do instead. + boolean advanced = end > start; send(sender, "{\"ok\":true,\"dim\":" + dim + ",\"startTick\":" + start + ",\"endTick\":" + end + ",\"elapsedTicks\":" + (end - start) + ",\"requested\":" + ticksToWait + + ",\"advanced\":" + advanced + + (advanced ? "" : ",\"hint\":\"the clock did not move: this handler runs on the " + + "server thread and cannot let it tick - poll from the test side instead\"") + ",\"wallMs\":" + (System.currentTimeMillis() - wallStart) + "}"); return; } diff --git a/src/main/java/zmaster587/advancedRocketry/entity/EntityRocket.java b/src/main/java/zmaster587/advancedRocketry/entity/EntityRocket.java index 7e3e4cafb..46cce49a1 100644 --- a/src/main/java/zmaster587/advancedRocketry/entity/EntityRocket.java +++ b/src/main/java/zmaster587/advancedRocketry/entity/EntityRocket.java @@ -1098,6 +1098,25 @@ public void tickFreeFlight() { in, thrustMag, gravity, canThrust); } + // ATMOSPHERE. Applied to whatever law just ran, because air does not ask which one it was. + // This is what bounds a craft's speed now that the law does not: the ceiling is a property of + // where you are, and in vacuum there is none. + // + // The STRICT lookup, deliberately: getDimensionProperties answers an unknown id with the + // OVERWORLD's properties, which carry a full atmosphere - so a space cell, a slot world or + // hyperspace would read as one-atmosphere air and quietly brake every ship flying through + // vacuum. A dimension that is not a registered body has no air here, which is also the + // physically right answer. + DimensionProperties atmProps = DimensionManager.getInstance() + .getDimensionPropertiesOrNull(this.world.provider.getDimension()); + double atmDensity = atmProps == null ? 0.0 : atmProps.getAtmosphereDensity() / 100.0; + if (atmDensity > 0.0) { + double[] dragged = FreeFlightPhysics.atmosphericDrag( + result.motionX, result.motionY, result.motionZ, atmDensity); + result = new FreeFlightPhysics.Step(dragged[0], dragged[1], dragged[2], + result.yaw, result.pitch, result.roll, result.thrustApplied); + } + // Engine power = magnitude of the thrust the engines applied this tick, // i.e. the world-frame Δv MINUS gravity (gravity is not thrust): the // difference between the resulting motion and where the craft would have diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/VSShipEntryRefusedKeepsPilotSeatedE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/VSShipEntryRefusedKeepsPilotSeatedE2ETest.java index 3c79754c7..dafd13447 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/VSShipEntryRefusedKeepsPilotSeatedE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/VSShipEntryRefusedKeepsPilotSeatedE2ETest.java @@ -58,6 +58,9 @@ public class VSShipEntryRefusedKeepsPilotSeatedE2ETest { Pattern.compile("\"builderPos\":\\[(-?\\d+),(-?\\d+),(-?\\d+)]"); private static final Pattern POS_Y = Pattern.compile("\"posY\":(-?[0-9.E\\-]+)"); private static final Pattern DUMMY_ID = Pattern.compile("\"dummyId\":(-?\\d+)"); + /** Ledger #264 discriminator: the seat's own delivery counters, sampled across the climb. */ + private static final Pattern RECEIVED = Pattern.compile("\"received\":(\\d+)"); + private static final Pattern DELIVERED = Pattern.compile("\"delivered\":(\\d+)"); private static final Pattern LEDGER = Pattern.compile("\"ledger\":(-?\\d+)"); private static final Pattern SHIP_ID = Pattern.compile("\"id\":\"([0-9a-fA-F-]+)\""); private static final Pattern VEL_Y = Pattern.compile("\"velY\":(-?[0-9.E\\-]+)"); @@ -201,6 +204,7 @@ public void aRefusedEntryLeavesThePilotSeatedWithAMessage() throws Exception { String refusalLine = null; double maxShipY = yRest; StringBuilder climb = new StringBuilder(64); + StringBuilder diag = new StringBuilder(64); bot().holdKey(Keyboard.KEY_R); try { for (int attempt = 0; attempt < budget && (yControl - yRest) < MIN_CONTROL_CLIMB; attempt++) { @@ -233,6 +237,19 @@ public void aRefusedEntryLeavesThePilotSeatedWithAMessage() throws Exception { // force-loads the ship's subspace yard nor touches a chunk, so the climb it is // watching gets exactly the resources it would have got unwatched. String s = exec("artest vs ship-info 0 id " + shipUuid); + // THE DISCRIMINATOR for ledger #264, sampled ACROSS the dying climb rather than + // after it. Three candidate causes, and the climb trace alone cannot separate + // them: the tile instance is being replaced under the ship (afcIdentity changes), + // the computer is not ticking at all (controllerTicks flat), or the packet + // arrives and is refused at the seat's pilot guard (received climbs while + // delivered does not). Sampled at the same cadence as the altitude so the two + // timelines line up tick for tick. + if (diag.length() < 900) { + String d = exec("artest vs seat-delivery"); + diag.append(' ').append(attempt).append(":recv=") + .append(firstGroupOr(RECEIVED, d, "?")) + .append("/deliv=").append(firstGroupOr(DELIVERED, d, "?")); + } Matcher py = POS_Y.matcher(s); Matcher vy = VEL_Y.matcher(s); if (py.find()) { @@ -263,7 +280,9 @@ public void aRefusedEntryLeavesThePilotSeatedWithAMessage() throws Exception { assertTrue("a pilot whose entry is refused (pool exhausted) must be TOLD so in his own " + "chat - a silent refusal reads as a dead ship. chat=" + bot().reportChat(8) + " subsystem=" + exec("artest space subsystem-status") - + " maxShipY=" + maxShipY + " climb(attempt:y/velY)=[" + climb.toString().trim() + + " maxShipY=" + maxShipY + + " delivery(attempt:recv/deliv)=[" + diag.toString().trim() + "]" + + " climb(attempt:y/velY)=[" + climb.toString().trim() + "] gate=" + exec("artest space entry-gate 0 " + shipUuid), refusalLine != null); @@ -302,6 +321,13 @@ private ClientBot bot() { return clientHarness.bot(); } + /** First capture group of {@code p} in {@code s}, or {@code fallback} — a missing field must read + * as "not answered" and never as a number, which is how a dead probe reads as a real zero. */ + private static String firstGroupOr(Pattern p, String s, String fallback) { + Matcher m = p.matcher(s); + return m.find() ? m.group(1) : fallback; + } + private String exec(String cmd) throws Exception { return String.join("\n", serverHarness.client().execute(cmd)); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/FreeFlightAssistsE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/FreeFlightAssistsE2ETest.java index 3a2461cd5..08eb2ec7d 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/FreeFlightAssistsE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/FreeFlightAssistsE2ETest.java @@ -204,6 +204,14 @@ public void yawingTheCraftRotatesTheCruiseVelocity() throws Exception { public void reEnablingFlightAssistCapturesTheCurrentVelocity() throws Exception { // Toggling FA back on mid-flight must NOT jerk the craft: the setpoint // initialises to the current velocity (Elite behaviour). + // + // AMENDED 2026-08-17. This test asserted the capture for a cruise ABOVE the assist's own + // ceiling, and that promise no longer exists: the acceleration law moved the ceiling ONTO the + // setpoint (FA_SETPOINT_MAX_SPEED), so re-engaging the assist above it deliberately decelerates + // the craft to it at the thrust budget rather than rewriting its velocity. The old assertion + // had been failing since that change landed and nobody read it — the cruise built here is 4.0 + // against a ceiling of 3.0. The capture is still the contract; it is now tested where the + // contract holds, and the clamp is tested beside it as its own leg. int id = buildAndAssemble(4350, 64, 500); ok(client().execute("artest rocket set-flight-mode " + id + " FREE_FLIGHT")); ok(client().execute("artest rocket start-free-flight " + id)); @@ -223,12 +231,17 @@ public void reEnablingFlightAssistCapturesTheCurrentVelocity() throws Exception // FA off, build a Newtonian cruise with direct thrust, then coast. ok(client().execute("artest rocket set-flight-assist " + id + " off")); ok(client().execute("artest rocket free-flight-input " + id + " 1 0 0 0 0")); - ok(client().execute("artest rocket free-flight-tick " + id + " 8")); + // Four ticks of thrust, not eight: 4 × 0.5 = 2.0 b/t, comfortably UNDER the assist ceiling, + // which is the regime where "capture the current velocity" is the promise. + ok(client().execute("artest rocket free-flight-tick " + id + " 4")); ok(client().execute("artest rocket free-flight-input " + id + " 0 0 0 0 0")); ok(client().execute("artest rocket free-flight-tick " + id + " 2")); double mzBefore = parseDouble(ok(client().execute("artest rocket info " + id)), Pattern.compile("\"motionZ\":(-?[0-9.E\\-]+)"), "motionZ"); assertTrue("precondition: must be coasting (+Z), got " + mzBefore, mzBefore > 0.2); + assertTrue("precondition: this leg tests the capture, so the cruise must be UNDER the assist " + + "ceiling (" + mzBefore + " vs 3.0) — above it the contract is the clamp below", + mzBefore < 3.0); // FA back on -> setpoint captured -> cruise continues, no jerk. ok(client().execute("artest rocket set-flight-assist " + id + " on")); @@ -239,6 +252,44 @@ public void reEnablingFlightAssistCapturesTheCurrentVelocity() throws Exception + mzAfter + ")", Math.abs(mzAfter - mzBefore) < 0.25); } + /** + * The other side of the same toggle, and the behaviour that replaced the old promise: re-engaging + * the assist on a craft flying FASTER than the assist's ceiling pulls it down to that ceiling — + * by thrusting against its motion, which is why it is a deceleration and not a rewrite. + */ + @Test + public void reEnablingFlightAssistAboveItsCeilingDeceleratesToTheCeiling() throws Exception { + int id = buildAndAssemble(4375, 64, 500); + ok(client().execute("artest rocket set-flight-mode " + id + " FREE_FLIGHT")); + ok(client().execute("artest rocket start-free-flight " + id)); + + // Climb clear of the ground, then hover, exactly as the capture leg does. + ok(client().execute("artest rocket free-flight-input " + id + " 0 1 0 0 0")); + ok(client().execute("artest rocket free-flight-tick " + id + " 60")); + ok(client().execute("artest rocket free-flight-input " + id + " 0 0 0 0 0 1")); + ok(client().execute("artest rocket free-flight-tick " + id + " 30")); + + // FA off, build a cruise well ABOVE the assist ceiling (8 × 0.5 = 4.0 against 3.0). + ok(client().execute("artest rocket set-flight-assist " + id + " off")); + ok(client().execute("artest rocket free-flight-input " + id + " 1 0 0 0 0")); + ok(client().execute("artest rocket free-flight-tick " + id + " 8")); + ok(client().execute("artest rocket free-flight-input " + id + " 0 0 0 0 0")); + ok(client().execute("artest rocket free-flight-tick " + id + " 2")); + double mzBefore = parseDouble(ok(client().execute("artest rocket info " + id)), + Pattern.compile("\"motionZ\":(-?[0-9.E\\-]+)"), "motionZ"); + assertTrue("precondition: the cruise must exceed the assist ceiling, got " + mzBefore, + mzBefore > 3.0); + + ok(client().execute("artest rocket set-flight-assist " + id + " on")); + ok(client().execute("artest rocket free-flight-tick " + id + " 20")); + double mzAfter = parseDouble(ok(client().execute("artest rocket info " + id)), + Pattern.compile("\"motionZ\":(-?[0-9.E\\-]+)"), "motionZ"); + assertTrue("the assist must bring an overfast craft DOWN toward its ceiling (was " + mzBefore + + ", now " + mzAfter + ")", mzAfter < mzBefore); + assertTrue("and must not overshoot below it — it tracks the ceiling, it does not brake to a " + + "halt (now " + mzAfter + ")", mzAfter > 2.0); + } + @Test public void flightAssistOffStillAcceptsExplicitBrake() throws Exception { // Cross-side wiring: FA=off + brake input still attenuates motion. diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/ServerWaitProbeReportsRealTicksTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/ServerWaitProbeReportsRealTicksTest.java new file mode 100644 index 000000000..4555fb803 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/server/ServerWaitProbeReportsRealTicksTest.java @@ -0,0 +1,69 @@ +package zmaster587.advancedRocketry.test.server; + +import org.junit.Test; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertTrue; + +/** + * Does {@code artest server wait} measure the WORLD, or does it measure itself? + * + *

    Measured 2026-08-17 on a space slot world: {@code wait 60} reported + * {@code elapsedTicks=0} after 12 s of wall clock, and a test built on that reading spent two + * revisions hunting a crossing bug that did not exist. Two causes fit equally: the slot world really + * does not tick (headless, no player, no ticking chunks), or the probe polls + * {@code getTotalWorldTime()} from the command — i.e. on the server thread, the one thread that + * advances it — and so blocks its own subject.

    + * + *

    This is that discriminator, asked of a world nobody doubts. Green = the probe reports real + * elapsed ticks on a ticking world, so a zero elsewhere is a fact about that world. Red = the + * probe cannot observe ticks at all and every reading it has ever produced is its own reflection.

    + * + *

    It stays in the suite rather than being deleted with its answer: what it pins is a HARNESS + * contract that other tests read as ground truth, and the day it starts failing is the day those + * tests begin measuring nothing.

    + */ +public class ServerWaitProbeReportsRealTicksTest extends AbstractSharedServerTest { + + /** Small enough to stay fast, large enough that a scheduler hiccup cannot fake it. */ + private static final int TICKS = 20; + + /** + * ANSWERED 2026-08-17: it measures itself. On the OVERWORLD — a world that ticks by definition — + * the probe reported zero elapsed ticks, so the handler runs on the very thread that advances the + * clock and can never see it move. Every "wait N ticks" in the suite has been a sleep. + * + *

    What this test pins now is therefore not "the clock advances" (it cannot, until the probe is + * rebuilt) but the property that keeps the next reader out of the same hole: the probe must SAY + * that it did not advance. A reply claiming success with no such field is what cost this + * session two wrong diagnoses.

    + */ + @Test + public void theWaitProbeNeverClaimsTicksItDidNotObserve() throws Exception { + String reply = exec("artest server wait 0 " + TICKS); + assertTrue("the wait probe failed on the overworld: " + reply, reply.contains("\"requested\"")); + + int elapsed = extractInt(reply, "elapsedTicks"); + boolean claimsAdvanced = reply.contains("\"advanced\":true"); + if (elapsed >= TICKS) { + assertTrue("the clock DID advance, so the probe must say so — a real wait that reports " + + "itself as a non-wait is the same defect mirrored: " + reply, claimsAdvanced); + return; + } + assertTrue("the probe returned fewer ticks than asked and must not report that as a wait: " + + reply, reply.contains("\"advanced\":false")); + assertTrue("and it must name what to do instead, or the next caller repeats the mistake: " + + reply, reply.contains("\"hint\"")); + } + + private String exec(String cmd) throws Exception { + return String.join("\n", client().execute(cmd)); + } + + private static int extractInt(String json, String key) { + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+)").matcher(json); + return m.find() ? Integer.parseInt(m.group(1)) : Integer.MIN_VALUE; + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/AtmosphericDragTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/AtmosphericDragTest.java new file mode 100644 index 000000000..1053ac97a --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/AtmosphericDragTest.java @@ -0,0 +1,97 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import zmaster587.advancedRocketry.api.FreeFlightPhysics; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for {@link FreeFlightPhysics#atmosphericDrag} — the bound that replaced the speed cap. + * + *

    Free flight is bounded by acceleration and not by speed, which leaves one hole: a craft may + * arrive at a planet arbitrarily fast and nothing charges it. An atmosphere charges it. What is pinned + * here is that the charge behaves like air — it opposes motion, scales with density, never turns a + * craft and never pushes it backwards — and that the drag constant means what its derivation says.

    + */ +public class AtmosphericDragTest { + + private static final double EPS = 1e-9; + + private static double speed(double[] v) { + return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + } + + @Test + public void vacuumChangesNothing() { + double[] v = FreeFlightPhysics.atmosphericDrag(30.0, -12.0, 4.0, 0.0); + assertEquals(30.0, v[0], EPS); + assertEquals(-12.0, v[1], EPS); + assertEquals(4.0, v[2], EPS); + + double[] negative = FreeFlightPhysics.atmosphericDrag(30.0, -12.0, 4.0, -1.0); + assertEquals("a negative density is vacuum, not thrust", 30.0, negative[0], EPS); + } + + /** + * The derivation itself: at the stated terminal speed in one atmosphere, drag must exactly cancel + * full thrust — that is what makes it a TERMINAL speed rather than a number someone liked. Read + * from the class, so re-deriving either input keeps this honest. + */ + @Test + public void atTheTerminalSpeedDragCancelsFullThrust() { + double vTerm = FreeFlightPhysics.ATMOSPHERIC_TERMINAL_SPEED; + double[] after = FreeFlightPhysics.atmosphericDrag(vTerm, 0.0, 0.0, 1.0); + double lost = vTerm - after[0]; + assertEquals("drag at terminal speed must equal the thrust budget, or the constant is not " + + "the one its derivation claims", + FreeFlightPhysics.MAX_THRUST_ACCEL, lost, 1e-9); + } + + @Test + public void dragOpposesMotionAndDoesNotTurnIt() { + double[] before = {12.0, -5.0, 3.0}; + double[] after = FreeFlightPhysics.atmosphericDrag(before[0], before[1], before[2], 1.0); + + assertTrue("air must slow a craft", speed(after) < speed(before)); + // Same direction: the cross product of the two velocity vectors is zero. + double cx = before[1] * after[2] - before[2] * after[1]; + double cy = before[2] * after[0] - before[0] * after[2]; + double cz = before[0] * after[1] - before[1] * after[0]; + assertEquals("drag may not steer", 0.0, Math.sqrt(cx * cx + cy * cy + cz * cz), 1e-9); + assertTrue("and may not reverse the craft", after[0] > 0.0 && after[1] < 0.0 && after[2] > 0.0); + } + + /** + * The clamp. An unclamped quadratic at high speed removes more velocity than the craft has, which + * would fly it backwards out of the atmosphere it just entered — a hull bouncing off the sky. + */ + @Test + public void airBringsACraftToRestButNeverThroughIt() { + double absurd = 100.0 * FreeFlightPhysics.ATMOSPHERIC_TERMINAL_SPEED; + double[] after = FreeFlightPhysics.atmosphericDrag(absurd, 0.0, 0.0, 1.0); + assertTrue("never reversed: " + after[0], after[0] >= 0.0); + assertTrue("and never faster than it arrived", after[0] <= absurd); + } + + @Test + public void denserAirBrakesHarder() { + double[] thin = FreeFlightPhysics.atmosphericDrag(50.0, 0.0, 0.0, 0.2); + double[] thick = FreeFlightPhysics.atmosphericDrag(50.0, 0.0, 0.0, 1.0); + assertTrue("a thicker atmosphere must take more speed: thin=" + thin[0] + " thick=" + thick[0], + thick[0] < thin[0]); + } + + /** + * Quadratic, not linear: doubling the speed must more than double the loss. Pinned because a + * linear drag would let a craft enter arbitrarily fast and lose a fixed fraction — which is the + * hole this closes, reopened. + */ + @Test + public void theLossGrowsWithTheSquareOfSpeed() { + double slowLoss = 20.0 - FreeFlightPhysics.atmosphericDrag(20.0, 0.0, 0.0, 1.0)[0]; + double fastLoss = 40.0 - FreeFlightPhysics.atmosphericDrag(40.0, 0.0, 0.0, 1.0)[0]; + assertEquals("twice the speed, four times the loss", 4.0, fastLoss / slowLoss, 1e-6); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PilotInputCadenceTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PilotInputCadenceTest.java new file mode 100644 index 000000000..4a2467f2f --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PilotInputCadenceTest.java @@ -0,0 +1,103 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import zmaster587.advancedRocketry.api.FreeFlightInput; +import zmaster587.advancedRocketry.api.PilotInputCadence; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for {@link PilotInputCadence} — when a pilot's command goes on the wire. + * + *

    What these pin is the property the mechanism exists for: a held command is re-asserted within + * a bounded time, so the cost of the server forgetting it is that bound and not the rest of the + * flight. The interval and the phase are read from the class rather than restated, so re-tuning + * changes the behaviour these tests describe without making them lie.

    + */ +public class PilotInputCadenceTest { + + private static FreeFlightInput held() { + return new FreeFlightInput(0f, 1f, 0f, 0f, 0f, 0f, 0f, false); + } + + @Test + public void aChangedInputGoesOutImmediately() { + assertTrue("the first input ever must be sent", + PilotInputCadence.shouldSend(held(), null, 1L, 0)); + assertTrue("a different input must be sent on the tick it changes", + PilotInputCadence.shouldSend(held(), FreeFlightInput.zero(), 7L, 0)); + } + + /** + * The defect this class was written for: a key held down, unchanged, while the server's copy of + * it is gone. Over any window as long as the repeat interval the command must be re-asserted at + * least once — asserted as a property of the window, not as "tick 20 specifically". + */ + @Test + public void aHeldInputIsReassertedWithinTheRepeatInterval() { + FreeFlightInput input = held(); + int phase = PilotInputCadence.phaseOfSeat(11, 64, -7); + + int sends = 0; + for (long tick = 1; tick <= PilotInputCadence.REPEAT_TICKS; tick++) { + if (PilotInputCadence.shouldSend(input, input, tick, phase)) { + sends++; + } + } + assertEquals("exactly one re-assert per interval — more is a burst, none is the bug", + 1, sends); + } + + @Test + public void anIdleInputIsNeverRepeated() { + FreeFlightInput idle = FreeFlightInput.zero(); + for (long tick = 0; tick <= 4L * PilotInputCadence.REPEAT_TICKS; tick++) { + assertFalse("releasing everything must not become a heartbeat: losing \"no input\" costs " + + "nothing, because no input is what the server falls back to", + PilotInputCadence.shouldSend(idle, idle, tick, 0)); + } + } + + @Test + public void nullIsNeverSent() { + assertFalse(PilotInputCadence.shouldSend(null, null, 0L, 0)); + } + + /** + * Two seats must not repeat on the same tick. Pinned because the failure is invisible in single + * play and only appears as a periodic spike on a busy server — the shape a shared {@code % N} + * clock always has. + */ + @Test + public void twoSeatsRepeatOnDifferentTicks() { + FreeFlightInput input = held(); + int phaseA = PilotInputCadence.phaseOfSeat(100, 70, 100); + int phaseB = PilotInputCadence.phaseOfSeat(101, 70, 100); + assertNotEquals("a seat one block over must land on a different phase", phaseA, phaseB); + + long tickA = -1, tickB = -1; + for (long tick = 1; tick <= PilotInputCadence.REPEAT_TICKS; tick++) { + if (tickA < 0 && PilotInputCadence.shouldSend(input, input, tick, phaseA)) { + tickA = tick; + } + if (tickB < 0 && PilotInputCadence.shouldSend(input, input, tick, phaseB)) { + tickB = tick; + } + } + assertTrue("both seats must re-assert inside one interval", tickA > 0 && tickB > 0); + assertNotEquals("two pilots must not stack their keep-alives onto one tick", tickA, tickB); + } + + @Test + public void thePhaseStaysInsideTheInterval() { + for (int x = -40; x <= 40; x++) { + int phase = PilotInputCadence.phaseOfSeat(x, -x, 3 * x); + assertTrue("a phase outside the interval would silently disable the repeat: " + phase, + phase >= 0 && phase < PilotInputCadence.REPEAT_TICKS); + } + } +} From 8ef837658ecfede5b1c5fe8d90c865cd5127bfb4 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 13:35:04 +0300 Subject: [PATCH 27/42] test: a wait that waits, and a test that stops pinning a moved constant - add artest server tick-count, an instant read of the clock - both server verbs report onServerThread, so the premise is measured - ServerTicks.await polls from the test jvm and throws on no advance - migrate 17 server wait sites and 2 bare sleeps - keep one deliberate caller: the verb's own test - derive the shipyard rungs from ShipChunkAllocator, not from 2026 values --- .../command/test/TestProbeCommand.java | 57 +++++++--- .../advancedRocketry/test/ServerTicks.java | 105 ++++++++++++++++++ .../SpikeFarCoordinatePlayabilityTest.java | 39 +++++-- .../SpikeFarCoordinateRenderJitterTest.java | 3 +- .../client/SpikeFarCoordinateShipTest.java | 3 +- .../SpikeSubBlockPositionGranularityTest.java | 7 +- .../test/server/AdvancementsTriggerTest.java | 10 +- .../test/server/LowGravFallDamageTest.java | 7 +- .../server/MixinHookBehaviourPinsTest.java | 2 +- .../test/server/RocketDescentLandingTest.java | 20 ++-- .../RocketEventPayloadContractTest.java | 6 +- .../ServerWaitProbeReportsRealTicksTest.java | 31 ++++++ .../SpikeFarCoordinateIntegrityTest.java | 5 +- 13 files changed, 239 insertions(+), 56 deletions(-) create mode 100644 src/test/java/zmaster587/advancedRocketry/test/ServerTicks.java diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 7aa779658..ab5aeaf39 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -20573,16 +20573,35 @@ private void handleChunk(MinecraftServer server, ICommandSender sender, String[] send(sender, "{\"error\":\"unknown chunk subcommand\"}"); } - // Server tick-wait probe ------------------------------------------- + // Server clock probes ---------------------------------------------- // - // companion to the chunk-anchor probe. Once the - // rocket's chunk is force-loaded, we need to let the server's - // natural tick loop run N times so EntityRocket.onUpdate is invoked - // in its production context (rather than driving it synthetically - // via /artest rocket tick). This probe polls - // world.getTotalWorldTime() until the configured number of ticks - // has elapsed, sleeping 50ms between polls. + // Companion to the chunk-anchor probe. Once the rocket's chunk is force-loaded, a test needs the + // server's natural tick loop to run N times so EntityRocket.onUpdate is invoked in its production + // context (rather than driving it synthetically via /artest rocket tick). + // + // A command handler CANNOT provide that wait, and the reason is structural rather than incidental: + // console commands are drained on the server thread, which is the one thread that advances + // world time, so any handler that blocks waiting for the clock is blocking the clock. `tick-count` + // is therefore the instant read, and the waiting belongs to the TEST thread, which is free while + // the server ticks. Both verbs report `onServerThread` so the claim is measured on every call + // rather than asserted in a comment — an earlier comment here asserted the opposite and was + // believed for months. private void handleServer(MinecraftServer server, ICommandSender sender, String[] args) { + // /artest server tick-count — one instant read of the world's own clock. This is the + // observable a test-side wait is built from: read, sleep in the TEST jvm, read again. + if (args.length >= 2 && "tick-count".equalsIgnoreCase(args[0])) { + int dim = parseIntOr(args[1], Integer.MIN_VALUE); + net.minecraft.world.WorldServer world = server.getWorld(dim); + if (world == null) { + send(sender, "{\"error\":\"world not loaded\",\"dim\":" + dim + "}"); + return; + } + send(sender, "{\"ok\":true,\"dim\":" + dim + + ",\"tick\":" + world.getTotalWorldTime() + + ",\"worldTime\":" + world.getWorldInfo().getWorldTime() + + ",\"onServerThread\":" + server.isCallingFromMinecraftThread() + "}"); + return; + } if (args.length >= 3 && "wait".equalsIgnoreCase(args[0])) { int dim = parseIntOr(args[1], Integer.MIN_VALUE); int ticksToWait = parseIntOr(args[2], 0); @@ -20623,19 +20642,23 @@ private void handleServer(MinecraftServer server, ICommandSender sender, String[ + ",\"elapsedTicks\":" + (end - start) + ",\"requested\":" + ticksToWait + ",\"advanced\":" + advanced + + ",\"onServerThread\":" + server.isCallingFromMinecraftThread() + (advanced ? "" : ",\"hint\":\"the clock did not move: this handler runs on the " - + "server thread and cannot let it tick - poll from the test side instead\"") + + "server thread and cannot let it tick - use 'server tick-count ' " + + "and wait from the test side instead\"") + ",\"wallMs\":" + (System.currentTimeMillis() - wallStart) + "}"); return; } // Block the server's TICK LOOP for a while, the way a real overloaded server does. // - // Probe handlers do not run on the server thread (the wait verb above polls the world clock - // from a command thread and would deadlock otherwise), so the block has to be scheduled ONTO - // that thread. Vanilla then logs its own "Can't keep up! ... skipping N tick(s)" and resumes, - // which is the whole point: a per-tick threshold anywhere in the codebase means something - // different across a tick that really took three seconds, and until now nothing in the harness - // could produce one. Bounded to 10 s so it can never approach the harness's command timeout. + // The block is scheduled onto the server thread rather than run inline. That is belt and + // braces, not necessity: handlers ALREADY run on the server thread (measured 2026-08-17 — the + // wait verb above cannot see the overworld clock move), and `addScheduledTask` invoked from + // that thread runs its runnable immediately, so this path stalls the loop either way. + // Vanilla then logs its own "Can't keep up! ... skipping N tick(s)" and resumes, which is the + // whole point: a per-tick threshold anywhere in the codebase means something different across + // a tick that really took three seconds, and until now nothing in the harness could produce + // one. Bounded to 10 s so it can never approach the harness's command timeout. if (args.length >= 2 && "stall".equalsIgnoreCase(args[0])) { long ms = parseIntOr(args[1], 0); if (ms <= 0L || ms > 10_000L) { @@ -20693,8 +20716,8 @@ public void run() { } return; } - send(sender, "{\"error\":\"usage: /artest server wait | save-dimensions\"}"); - send(sender, "{\"error\":\"usage: /artest server wait | /artest server stall \"}"); + send(sender, "{\"error\":\"usage: /artest server tick-count | wait " + + "| stall | save-dimensions\"}"); } /** True if the {@code .class} resource is reachable via the diff --git a/src/test/java/zmaster587/advancedRocketry/test/ServerTicks.java b/src/test/java/zmaster587/advancedRocketry/test/ServerTicks.java new file mode 100644 index 000000000..826254c5e --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/ServerTicks.java @@ -0,0 +1,105 @@ +package zmaster587.advancedRocketry.test; + +import com.github.stannismod.forge.testing.TestTimeouts; +import com.github.stannismod.forge.testing.server.TestClient; + +import java.time.Duration; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A wait that actually waits: let the SERVER's world clock advance by N ticks. + * + *

    Console commands are drained on the server thread — the one thread that advances world time — + * so a probe handler that polls the clock is blocking the very thing it is watching. Measured + * 2026-08-17: {@code artest server wait 0 20} reports {@code elapsedTicks=0} on the OVERWORLD, a + * world that ticks by definition. Every call site that read that verb as "N ticks have now happened" + * was reading a sleep, and two wrong diagnoses were paid for it.

    + * + *

    So the waiting lives here, in the TEST jvm, which is idle while the server ticks: read the + * clock, sleep, read again. The same shape the client half already uses — {@code ClientBot.waitTicks} + * polls a counter from the bridge thread rather than from the client thread.

    + * + *

    What a caller gets that a sleep never gave it: the returned value is observed, off the + * world's own clock, and a wait that does not happen fails loudly instead of passing silently.

    + */ +public final class ServerTicks { + + /** One game tick, nominal. The server may be slower; it is never faster. */ + private static final long TICK_MS = 50L; + + /** + * How much longer than nominal a wait may take before it is called a failure. A headless server + * under concurrent forks runs behind, and {@link TestTimeouts} scales this again by fork count — + * this factor covers ordinary slack (chunk loads, GC), not contention. + */ + private static final int SLACK_FACTOR = 4; + + /** Floor for the ceiling: a short wait still gets room for one slow round-trip. */ + private static final Duration MIN_BUDGET = Duration.ofSeconds(3); + + /** Ceiling for the ceiling — a runaway wait must surface as a red, not as a hung suite. */ + private static final Duration MAX_BUDGET = Duration.ofSeconds(60); + + private static final Pattern TICK_FIELD = Pattern.compile("\"tick\":(-?\\d+)"); + + private ServerTicks() { } + + /** The world's own clock, right now. One round-trip, no waiting. */ + public static long count(TestClient client, int dim) throws Exception { + String reply = String.join("\n", client.execute("artest server tick-count " + dim)); + Matcher matcher = TICK_FIELD.matcher(reply); + if (!matcher.find()) { + throw new AssertionError("artest server tick-count " + dim + + " did not report a clock (is the dimension loaded?): " + reply); + } + return Long.parseLong(matcher.group(1)); + } + + /** + * Block the calling test until dimension {@code dim}'s clock has advanced by at least + * {@code ticks}, and return how far it actually advanced (never less than {@code ticks}). + * + * @throws AssertionError if the clock does not get there inside the budget — which is the + * interesting case, and the one the old probe reported as success. + */ + public static long await(TestClient client, int dim, int ticks) throws Exception { + return await(client, dim, ticks, budgetFor(ticks)); + } + + /** As {@link #await(TestClient, int, int)}, with a caller-chosen ceiling. */ + public static long await(TestClient client, int dim, int ticks, Duration budget) throws Exception { + if (ticks <= 0) { + throw new IllegalArgumentException("ticks must be positive, got " + ticks); + } + long start = count(client, dim); + long target = start + ticks; + long deadlineNanos = System.nanoTime() + budget.toNanos(); + + long observed = start; + while (observed < target) { + if (System.nanoTime() > deadlineNanos) { + throw new AssertionError("dim " + dim + " advanced only " + (observed - start) + + " of the " + ticks + " ticks asked for, inside " + budget.toMillis() + + " ms. Either the world is not ticking (no players, no forced chunks," + + " a slot world nobody drives) or the server is stalled — both are" + + " findings, and neither is a wait."); + } + // Sleep the time the remaining ticks would take at nominal rate, so a long wait costs + // one or two round-trips rather than one per tick. Bounded so a slow server is noticed + // early rather than at the deadline. + long remaining = target - observed; + Thread.sleep(Math.max(TICK_MS, Math.min(500L, remaining * TICK_MS))); + observed = count(client, dim); + } + return observed - start; + } + + /** The default ceiling for {@code ticks}: nominal duration with slack, load-scaled and clamped. */ + static Duration budgetFor(int ticks) { + Duration nominal = Duration.ofMillis(ticks * TICK_MS * SLACK_FACTOR); + Duration base = nominal.compareTo(MIN_BUDGET) < 0 ? MIN_BUDGET : nominal; + Duration scaled = TestTimeouts.scaled(base); + return scaled.compareTo(MAX_BUDGET) > 0 ? MAX_BUDGET : scaled; + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinatePlayabilityTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinatePlayabilityTest.java index 150306cdd..45c0b97b3 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinatePlayabilityTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinatePlayabilityTest.java @@ -5,6 +5,8 @@ import org.junit.Test; import org.lwjgl.input.Keyboard; +import org.valkyrienskies.mod.common.ships.chunk_claims.ShipChunkAllocator; +import zmaster587.advancedRocketry.test.ServerTicks; import java.nio.file.Files; import java.nio.file.Path; @@ -109,10 +111,17 @@ private String exec(String cmd) throws Exception { /** * Pins the delivery wall against numbers predicted from the physics mod's own predicate, so the * mechanism is proven rather than inferred. {@code isChunkInShipyard(cx, cz)} is - * {@code cx >= CHUNK_X_START - MAX_CHUNK_RADIUS && cz >= CHUNK_Z_START - MAX_CHUNK_RADIUS} - * = {@code cx >= 318401 && cz >= -1599}, so the four cases below are decided before the run: - * one block under the X edge moves, one block over it does not, and the same X moves again once - * Z drops below the quadrant. A miss on ANY of the four falsifies the explanation. + * {@code cx >= CHUNK_X_START - MAX_CHUNK_RADIUS && cz >= CHUNK_Z_START - MAX_CHUNK_RADIUS}, so + * the four cases below are decided before the run: one chunk under the X edge moves, the first + * reserved chunk does not, and a coordinate deep inside moves again once Z drops below the + * quadrant. A miss on ANY of the four falsifies the explanation. + * + *

    The edge is READ from the allocator rather than written down. It was written down once — + * as {@code cx >= 318401}, block X 5 094 416 — and then the constant was raised to give the + * cell its clearance, at which point this test went red saying the explanation had been + * falsified. It had not: the number had moved and the test had not been told. A test that + * pins a mechanism must be keyed to the mechanism's own constant, or it pins the day it was + * written.

    */ @Test public void whereExactlyDoesADeliveryStopWorking() throws Exception { @@ -122,12 +131,18 @@ public void whereExactlyDoesADeliveryStopWorking() throws Exception { List report = new ArrayList<>(); List wrong = new ArrayList<>(); + // The first reserved BLOCK X, straight out of the predicate the teleport is cancelled by. + final long edgeX = ((long) (ShipChunkAllocator.CHUNK_X_START + - ShipChunkAllocator.MAX_CHUNK_RADIUS)) << 4; + // Deep inside the quadrant, and derived so it stays inside whatever the edge becomes — + // a hard-coded 28M was inside the old quadrant and would not be inside a much later one. + final long deepX = edgeX + 1_000_000L; // {x, z, expectedToMove} double[][] cases = { - {5_094_400.5d, 0.5d, 1d}, // chunkX 318400 — one chunk under the edge - {5_094_416.5d, 0.5d, 0d}, // chunkX 318401 — the first reserved chunk - {28_000_000.5d, 0.5d, 0d}, // deep inside the quadrant - {28_000_000.5d, ARENA_Z + 0.5d, 1d}, // same X, Z below the quadrant's edge + {edgeX - 16 + 0.5d, 0.5d, 1d}, // one chunk under the edge + {edgeX + 0.5d, 0.5d, 0d}, // the first reserved chunk + {deepX + 0.5d, 0.5d, 0d}, // deep inside the quadrant + {deepX + 0.5d, ARENA_Z + 0.5d, 1d}, // same X, Z below the quadrant's edge }; for (double[] c : cases) { boolean expectMove = c[2] != 0d; @@ -145,7 +160,7 @@ public void whereExactlyDoesADeliveryStopWorking() throws Exception { } // Park him back near the origin so the next case starts from a known place. exec("artest player far-tp 0.5 200 0.5"); - exec("artest server wait " + OVERWORLD + " 20"); + ServerTicks.await(serverClient(), OVERWORLD, 20); } StringBuilder out = new StringBuilder("[SPIKE far-coordinate delivery boundary]\n"); @@ -273,7 +288,7 @@ private void buildArena(int x) throws Exception { exec("artest chunk forceload " + OVERWORLD + " " + cx + " " + cz); } } - exec("artest server wait " + OVERWORLD + " 60"); + ServerTicks.await(serverClient(), OVERWORLD, 60); int x1 = x - 4; int x2 = x + WALL_OFFSET + 4; @@ -282,7 +297,7 @@ private void buildArena(int x) throws Exception { // Hollow out everything up to (but not including) the wall plane at x+WALL_OFFSET. exec("artest fill " + OVERWORLD + " " + (x1 + 1) + " " + STAND_Y + " " + (ARENA_Z - 5) + " " + (x + WALL_OFFSET - 1) + " " + (FLOOR_Y + 5) + " " + (ARENA_Z + 5) + " minecraft:air"); - exec("artest server wait " + OVERWORLD + " 20"); + ServerTicks.await(serverClient(), OVERWORLD, 20); } /** @@ -338,7 +353,7 @@ private String deliverAndStand(int x) throws Exception { for (int attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt++) { lastReply = exec("artest player far-tp " + fmt(x + 0.5d) + " " + STAND_Y + " " + fmt(ARENA_Z + 0.5d)); - exec("artest server wait " + OVERWORLD + " 40"); + ServerTicks.await(serverClient(), OVERWORLD, 40); bot().waitTicks(30); lastX = serverX(); lastY = serverY(); diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java index 831b6db69..bbc184b3e 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateRenderJitterTest.java @@ -4,6 +4,7 @@ import com.google.gson.JsonObject; import org.junit.Test; +import zmaster587.advancedRocketry.test.ServerTicks; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; @@ -283,7 +284,7 @@ private double deliver(int x, int y) throws Exception { double actualX = Double.NaN; for (int attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt++) { exec("artest player far-tp " + fmt(x + 0.5d) + " " + y + " " + fmt(ARENA_Z + 0.5d)); - exec("artest server wait " + OVERWORLD + " 60"); + ServerTicks.await(serverClient(), OVERWORLD, 60); bot().waitTicks(20); actualX = posXOf(exec("artest player health")); if (Math.abs(actualX - (x + 0.5d)) < 2d) { diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateShipTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateShipTest.java index 5963d1586..d54e84313 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateShipTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeFarCoordinateShipTest.java @@ -5,6 +5,7 @@ import org.junit.Assume; import org.junit.Test; import org.lwjgl.input.Keyboard; +import zmaster587.advancedRocketry.test.ServerTicks; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -575,7 +576,7 @@ private String deliver(int x) throws Exception { for (int attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt++) { exec("artest player far-tp " + fmt(x + 0.5d) + " " + (BASE_Y + 6) + " " + fmt(ARENA_Z + 0.5d)); - exec("artest server wait 0 40"); + ServerTicks.await(serverClient(), 0, 40); bot().waitTicks(30); lastX = field(exec("artest player health"), "posX"); if (Math.abs(lastX - (x + 0.5d)) < ARRIVAL_TOLERANCE) { diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeSubBlockPositionGranularityTest.java b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeSubBlockPositionGranularityTest.java index b79236731..32dd56738 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/SpikeSubBlockPositionGranularityTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/SpikeSubBlockPositionGranularityTest.java @@ -4,6 +4,7 @@ import com.google.gson.JsonObject; import org.junit.Test; +import zmaster587.advancedRocketry.test.ServerTicks; import java.util.ArrayList; import java.util.List; @@ -141,7 +142,7 @@ public void doesASubBlockPositionSurviveTheRoundTripFarFromTheOrigin() throws Ex for (double offset : OFFSETS) { double target = x + 0.5d + offset; exec("tp " + botName + " " + fmt(target) + " " + STAND_Y + " " + fmt(ARENA_Z + 0.5d)); - exec("artest server wait " + OVERWORLD + " 6"); + ServerTicks.await(serverClient(), OVERWORLD, 6); bot().waitTicks(6); double gotServer = serverX(); @@ -200,7 +201,7 @@ public void doesASubBlockPositionSurviveTheRoundTripFarFromTheOrigin() throws Ex private void buildFloor(int x) throws Exception { exec("artest chunk forceload " + OVERWORLD + " " + (x >> 4) + " " + (ARENA_Z >> 4)); - exec("artest server wait " + OVERWORLD + " 20"); + ServerTicks.await(serverClient(), OVERWORLD, 20); exec("artest fill " + OVERWORLD + " " + (x - 4) + " " + FLOOR_Y + " " + (ARENA_Z - 4) + " " + (x + 4) + " " + FLOOR_Y + " " + (ARENA_Z + 4) + " minecraft:stone"); exec("artest fill " + OVERWORLD + " " + (x - 4) + " " + STAND_Y + " " + (ARENA_Z - 4) + " " @@ -239,7 +240,7 @@ private String deliverAndStand(int x) throws Exception { for (int attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt++) { lastReply = exec("artest player far-tp " + fmt(x + 0.5d) + " " + STAND_Y + " " + fmt(ARENA_Z + 0.5d)); - exec("artest server wait " + OVERWORLD + " 40"); + ServerTicks.await(serverClient(), OVERWORLD, 40); bot().waitTicks(30); lastX = serverX(); lastY = serverY(); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/AdvancementsTriggerTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/AdvancementsTriggerTest.java index d1d0c0399..4a2befc15 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/AdvancementsTriggerTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/AdvancementsTriggerTest.java @@ -6,6 +6,7 @@ import org.junit.Assume; import org.junit.Before; import org.junit.Test; +import zmaster587.advancedRocketry.test.ServerTicks; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -99,11 +100,10 @@ private void stationAndTick(int dim, double x, double y, double z, int ticks) th exec("artest chunk forceload " + dim + " " + (((int) x) >> 4) + " " + (((int) z) >> 4)); assertTrue("tick-living must succeed", exec("artest player tick-living " + ticks).contains("\"ok\":true")); - // Wait OFF the server thread: `artest server wait` runs inside a - // console command, i.e. ON the server thread — its sleep loop blocks - // ticking entirely. Sleeping in the test JVM lets the server - // free-run the requested ticks. - Thread.sleep(ticks * 50L + 500L); + // Wait OFF the server thread: a console command runs ON the server thread, so a probe that + // sleeps there blocks ticking entirely. The wait belongs in the test jvm — and it OBSERVES + // the world's clock rather than hoping for it, so a world that is not ticking says so. + ServerTicks.await(harness.client(), dim, ticks + 10); } private boolean isDone(String src) { diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/LowGravFallDamageTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/LowGravFallDamageTest.java index fdf325068..cef061210 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/LowGravFallDamageTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/LowGravFallDamageTest.java @@ -6,6 +6,7 @@ import org.junit.Assume; import org.junit.Before; import org.junit.Test; +import zmaster587.advancedRocketry.test.ServerTicks; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -83,9 +84,9 @@ private String exec(String cmd) throws Exception { private void stationFake(int dim) throws Exception { String fake = exec("artest player ensure-fake " + dim + " 8.5 120 8.5"); assertTrue("ensure-fake must succeed: " + fake, fake.contains("\"ok\":true")); - // Off-thread settle (see AdvancementsTriggerTest: `artest server wait` - // blocks the server thread and must not be used to advance ticks). - Thread.sleep(1000L); + // Off-thread settle: the wait runs in the test jvm, because a command handler runs on the + // server thread and would block the clock it is waiting for. + ServerTicks.await(harness.client(), dim, 20); } /** Overworld: not an IPlanetaryProvider → distance untouched. */ diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/MixinHookBehaviourPinsTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/MixinHookBehaviourPinsTest.java index 66945234a..589b18c2d 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/MixinHookBehaviourPinsTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/MixinHookBehaviourPinsTest.java @@ -176,7 +176,7 @@ private double doubleField(Pattern p, String src, String fieldName) { * *

    Robust against the dedicated-server harness's idiosyncratic * tick scheduling, which doesn't reliably advance entity onUpdate - * during {@code /artest server wait} on a cold server.

    + * within a bounded wait on a cold server.

    */ private double tickEntityAndReadMotionY(int dim, int id, int count) throws Exception { String resp = ok(client().execute( diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/RocketDescentLandingTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/RocketDescentLandingTest.java index 8ab50e233..1efd08d88 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/RocketDescentLandingTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/RocketDescentLandingTest.java @@ -1,6 +1,7 @@ package zmaster587.advancedRocketry.test.server; import org.junit.Test; +import zmaster587.advancedRocketry.test.ServerTicks; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,10 +26,11 @@ * (registered via {@code WorldEvents} mod-side, dispensed by the new * {@code /artest chunk forceload} probe). Holding the chunk hot lets * the headless dedicated server tick the rocket entity through its - * production code paths exactly as a real game session would. The - * {@code /artest server wait } probe blocks the test - * thread until {@code worldserver.getTotalWorldTime()} has advanced by - * the requested number of ticks. + * production code paths exactly as a real game session would. + * {@link zmaster587.advancedRocketry.test.ServerTicks#await} blocks the + * test thread until the world's own clock has advanced by the requested + * number of ticks — the waiting happens in the test jvm, because a + * command handler runs on the very thread that advances that clock. * *

    Test method names suffixed {@code _realTick} to make it explicit * which path is exercised. @@ -123,7 +125,7 @@ public void descentTimerGateFlipsInFlightUnderRealTicks_realTick() throws Except // Setup under REAL server ticking: // - assemble + force-load the rocket's chunk // - state: orbit=true, flight=false, ticksExisted=DESCENT_TIMER+1 - // - server wait 5 ticks -> onUpdate runs at least once -> + // - await 5 real ticks -> onUpdate runs at least once -> // gate fires -> isInFlight flips to true. int baseX = 6100; int baseZ = 500; @@ -134,7 +136,7 @@ public void descentTimerGateFlipsInFlightUnderRealTicks_realTick() throws Except + " orbit=true flight=false ticksExisted=" + (DESCENT_TIMER + 1) + " posY=300 motionY=0")); - ok(client().execute("artest server wait 0 5")); + ServerTicks.await(client(), 0, 5); String info = ok(client().execute("artest rocket info " + id)); assertTrue("descent gate must flip isInFlight under real ticking: " + info, @@ -154,7 +156,7 @@ public void tickBeforeDescentTimerKeepsFlightOff_realTick() throws Exception { ok(client().execute("artest rocket set-state " + id + " orbit=true flight=false ticksExisted=5 posY=300 motionY=0")); - ok(client().execute("artest server wait 0 5")); + ServerTicks.await(client(), 0, 5); String info = ok(client().execute("artest rocket info " + id)); // ticksExisted will have advanced by up to ~5 under real ticking; @@ -182,7 +184,7 @@ public void inFlightDescentApplesGravityUnderRealTicks_realTick() throws Excepti + " orbit=true flight=true ticksExisted=" + (DESCENT_TIMER + 5) + " posY=300 motionY=0")); - ok(client().execute("artest server wait 0 5")); + ServerTicks.await(client(), 0, 5); String info = ok(client().execute("artest rocket info " + id)); Matcher m = POS_Y_FIELD.matcher(info); @@ -216,7 +218,7 @@ public void landedEventFiresOnGroundCollisionUnderRealTicks_realTick() throws Ex + " orbit=true flight=true ticksExisted=" + (DESCENT_TIMER + 5) + " posY=" + (baseY + 2) + " motionY=-10")); - ok(client().execute("artest server wait 0 6")); + ServerTicks.await(client(), 0, 6); String countsAfter = ok(client().execute("artest rocket event-counts-full")); int landedAfter = gi(LANDED_COUNT, countsAfter, "landed after"); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/RocketEventPayloadContractTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/RocketEventPayloadContractTest.java index 06a2beb17..e89cde5ae 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/RocketEventPayloadContractTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/RocketEventPayloadContractTest.java @@ -2,6 +2,8 @@ import org.junit.Test; +import zmaster587.advancedRocketry.test.ServerTicks; + import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -143,7 +145,7 @@ public void rocketLandedEventCarriesRocketEntityAndWorld() throws Exception { exec("artest rocket set-state " + rocketId + " orbit=true flight=true ticksExisted=" + (DESCENT_TIMER + 5) + " posY=" + (CY + 2) + " motionY=-10"); - exec("artest server wait 0 6"); + ServerTicks.await(client(), 0, 6); String countsAfter = exec("artest rocket event-counts-full"); int landedAfter = extract(countsAfter, LANDED_COUNT); @@ -194,7 +196,7 @@ public void rocketDeOrbitingEventCarriesRocketEntityAndWorld() throws Exception // event. exec("artest rocket set-state " + rocketId + " orbit=true flight=false ticksExisted=18 posY=300 motionY=0"); - exec("artest server wait 0 3"); + ServerTicks.await(client(), 0, 3); String countsAfter = exec("artest rocket event-counts-full"); int deOrbitAfter = extract(countsAfter, DEORBIT_COUNT); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/ServerWaitProbeReportsRealTicksTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/ServerWaitProbeReportsRealTicksTest.java index 4555fb803..97911c171 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/ServerWaitProbeReportsRealTicksTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/ServerWaitProbeReportsRealTicksTest.java @@ -1,6 +1,7 @@ package zmaster587.advancedRocketry.test.server; import org.junit.Test; +import zmaster587.advancedRocketry.test.ServerTicks; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -58,6 +59,36 @@ public void theWaitProbeNeverClaimsTicksItDidNotObserve() throws Exception { + reply, reply.contains("\"hint\"")); } + /** + * The other half of the same contract: a test that asks for N ticks must be able to SEE the + * world's own clock move by N. The probe above cannot deliver that from the server thread, so the + * waiting lives in the test jvm ({@link ServerTicks}) and this is its acceptance — asked, again, + * of a world whose answer is not in doubt. + * + *

    Note what is asserted and what is not: the clock advanced by at least what was asked. Not + * how long it took, not that it stopped there. A wall-clock pin here would be a test of this + * machine's load, which is the very confusion the task exists to end.

    + */ + @Test + public void aTestSideWaitAdvancesTheWorldsOwnClock() throws Exception { + // The premise, measured rather than asserted in a comment: the handler answering this runs on + // the thread that advances the clock. That is WHY the wait cannot live in a probe, and it was + // once written down here the other way round and believed for months. + String clock = exec("artest server tick-count 0"); + assertTrue("a probe handler must report that it runs on the server thread — if this ever " + + "flips, a probe-side wait becomes possible and ServerTicks can be retired: " + clock, + clock.contains("\"onServerThread\":true")); + + long before = ServerTicks.count(client(), 0); + long observed = ServerTicks.await(client(), 0, TICKS); + long after = ServerTicks.count(client(), 0); + + assertTrue("the wait reported " + observed + " ticks but was asked for " + TICKS + + " — a wait may never return short", observed >= TICKS); + assertTrue("the overworld clock must have moved by at least " + TICKS + " ticks across the " + + "wait, but went " + before + " -> " + after, after - before >= TICKS); + } + private String exec(String cmd) throws Exception { return String.join("\n", client().execute(cmd)); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/SpikeFarCoordinateIntegrityTest.java b/src/test/java/zmaster587/advancedRocketry/test/server/SpikeFarCoordinateIntegrityTest.java index d1e548df6..e4e4d47d5 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/SpikeFarCoordinateIntegrityTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/SpikeFarCoordinateIntegrityTest.java @@ -3,6 +3,7 @@ import com.github.stannismod.forge.testing.junit.AbstractHeadlessServerTest; import org.junit.Test; +import zmaster587.advancedRocketry.test.ServerTicks; import java.util.ArrayList; import java.util.List; @@ -52,7 +53,7 @@ public void chunksAndBlockStorageStillWorkFarFromTheOrigin() throws Exception { exec("artest chunk forceload " + OVERWORLD + " " + chunkX + " 0"); // Generation at a fresh, distant chunk is not instant; give the server real ticks rather // than reading an empty chunk and calling it a ceiling. - exec("artest server wait " + OVERWORLD + " 40"); + ServerTicks.await(client(), OVERWORLD, 40); String sample = exec("artest worldgen sample " + OVERWORLD + " " + chunkX + " 0"); String placed = exec("artest place " + OVERWORLD + " " + x + " " + PLACE_Y + " 0 " @@ -99,7 +100,7 @@ public void doEntityDoublesHoldASubBlockXFarFromTheOrigin() throws Exception { List broken = new ArrayList<>(); for (int x : X_LADDER) { exec("artest chunk forceload " + OVERWORLD + " " + (x >> 4) + " 0"); - exec("artest server wait " + OVERWORLD + " 40"); + ServerTicks.await(client(), OVERWORLD, 40); String near = spawnAndRead(x + 0.5500d); String far = spawnAndRead(x + 0.6000d); From 414a9379d16524efd7302ee36eaca8ae0913bb1f Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 14:28:07 +0300 Subject: [PATCH 28/42] feat: a short jump is one crossing, not a flight through hyperspace - derive the threshold from the two phase windows, never a third number - one predicate decides, read by the console forecast and the departure - generalize the seam controller: a jump is a carry with a chosen cell - no lane, no snapshot, no IN_TRANSIT row for a crossing - require the fixture jump speed so no caller picks a mechanism blindly - give transit-tick a count and resolve its anchor by ship identity - measure the fixture cell spacing instead of writing it down --- .../command/test/TestProbeCommand.java | 83 +++++- .../navigation/ShipNavigation.java | 20 ++ .../space/CellCrossingController.java | 266 ++++++++++++++++++ .../advancedRocketry/space/CellSeam.java | 2 +- .../space/CellSeamController.java | 186 ------------ .../space/ShipTransitManager.java | 95 ++++++- .../space/SpaceSubsystem.java | 29 +- .../space/SpaceSubsystemEvents.java | 2 +- .../tile/TileAdvancedFlightComputer.java | 4 +- .../assets/advancedrocketry/lang/en_US.lang | 1 + .../assets/advancedrocketry/lang/ru_RU.lang | 1 + .../test/AdvancedRocketryTestConstants.java | 38 +++ .../VSFlightSmoothnessAcrossJumpE2ETest.java | 8 +- .../VSMidTransitRelogControlE2ETest.java | 9 +- .../client/VSTransitCrewGroupE2ETest.java | 39 +-- .../HyperspaceSurvivesARestartE2ETest.java | 4 +- .../VSJumpCarriesLooseBodiesE2ETest.java | 7 +- .../test/server/VSShipCellSeamE2ETest.java | 2 +- .../test/server/VSShipTransitE2ETest.java | 11 +- .../server/VSShipTransitPersistE2ETest.java | 6 +- .../VSShortJumpCrossesDirectlyE2ETest.java | 145 ++++++++++ ...nmannedTransitSettlesOnItsPoseE2ETest.java | 6 +- .../unit/ShortJumpCrossesDirectlyTest.java | 260 +++++++++++++++++ 23 files changed, 971 insertions(+), 253 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/space/CellCrossingController.java delete mode 100644 src/main/java/zmaster587/advancedRocketry/space/CellSeamController.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/server/VSShortJumpCrossesDirectlyE2ETest.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/ShortJumpCrossesDirectlyTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index ab5aeaf39..567ee5336 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -4032,6 +4032,14 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] // the ship is the one caller that never has to guess. java.util.UUID pilotedShip = zmaster587.advancedRocketry.integration.vs.VSIntegration.assembleTier2Ship(w, anchor); + // SETTLE the ship in this stack's own ledger, the way the entry on-ramp would have. Without + // it the fixture is a ship that is nowhere: production never has a craft sitting in a cell + // with no ledger row, and anything that asks the ledger where this ship IS - a short jump, + // a seam carry, a descent - correctly refuses to act on a ship it cannot place. Written on + // THIS stack's ledger, not the attached subsystem's: the two are different objects here. + if (transitDurableId != null) { + transitStack.ledger.settle(transitDurableId, transitOrigin); + } // Assembly is ASYNC (queued on the physics thread), so the seat + ship world pos are NOT queryable // yet. The caller polls `vs ship-count-all`/`load-ships`/`ship-count` for the ship, then reads the // post-assembly pilot-seat subspace pos + ship world pos via `vs find-seat id `. @@ -4041,12 +4049,19 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] + ",\"durableId\":\"" + (transitDurableId == null ? "" : transitDurableId) + "\"}"); return; } - // transit-begin [speedBlocksPerTick]: start the jump (arrival - // retries until the async hyperspace ship is crossable, so a large speed is fine). The - // optional speed lets a test SIZE the park: the setup cells sit one sector (4M blocks) - // apart, so the default 5M crosses in a single tick, while e.g. 100k parks the ship for - // ~40 probe-driven ticks — enough for a mid-transit stimulus (a relog) to land inside it. - if (args.length >= 5 && "transit-begin".equalsIgnoreCase(args[0])) { + // transit-begin : start the jump. + // + // The speed is REQUIRED, and it used to default to 5M. That default was harmless while there + // was one mechanism and it only sized the park; it stopped being harmless the moment the + // computed duration began choosing between hyperspace and a direct cell-to-cell crossing. + // At the setup's one-sector spacing (4M blocks) 5M crosses in a single tick, so the default + // silently picked the direct path for every caller that did not think about it — including + // every test written to exercise hyperspace. A caller now says which flight it wants. + // + // The arithmetic a caller needs: ticks = ceil(4M / speed), and a jump of at most + // ShipTransitManager.DIRECT_CROSSING_MAX_TICKS ticks is performed as one crossing. So + // speed >= 25_000 is a direct hop and speed <= 20_000 is a real flight with a park in it. + if (args.length >= 6 && "transit-begin".equalsIgnoreCase(args[0])) { if (transitTm == null) { send(sender, "{\"error\":\"transit not set up\"}"); return; @@ -4054,8 +4069,27 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] int originDim = parseIntOr(args[1], Integer.MIN_VALUE); net.minecraft.util.math.BlockPos anchor = new net.minecraft.util.math.BlockPos( parseIntOr(args[2], 0), parseIntOr(args[3], 0), parseIntOr(args[4], 0)); - long speed = args.length >= 6 - ? Math.max(1L, Long.parseLong(args[5])) : 5_000_000L; + // The caller names the BUILD pad, which is where the ship was assembled FROM — after + // assembly its blocks live in a subspace shipyard and the pad is empty air. Production's + // caller (JumpTrigger) never has this problem: it is the flight computer, so it passes its + // own live position. Resolve the same thing here, by IDENTITY rather than by proximity, so + // a departure that reads the ship's pose off this anchor reads a real block. + net.minecraft.world.WorldServer originWorld = + net.minecraftforge.common.DimensionManager.getWorld(originDim); + boolean anchorRelocated = false; + if (transitDurableId != null && originWorld != null) { + for (net.minecraft.tileentity.TileEntity te + : new java.util.ArrayList<>(originWorld.loadedTileEntityList)) { + if (te instanceof zmaster587.advancedRocketry.tile.TileAdvancedFlightComputer + && transitDurableId.equals(((zmaster587.advancedRocketry.tile + .TileAdvancedFlightComputer) te).shipIdOrNull())) { + anchor = te.getPos(); + anchorRelocated = true; + break; + } + } + } + long speed = Math.max(1L, Long.parseLong(args[5])); // Depart under the fixture's own DURABLE id, so the crossing resolves the ship it was told // about instead of whatever craft is nearest an anchor every scenario here reuses. The // synthetic "t" remains for fixtures that assembled nothing to name. @@ -4072,6 +4106,9 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] // about a client in the wrong dimension. send(sender, "{\"ok\":true,\"began\":" + began + ",\"shipId\":\"" + departingShip + "\",\"crew\":" + transitTm.crewCountOf(departingShip) + + ",\"anchorRelocated\":" + anchorRelocated + + ",\"anchorX\":" + anchor.getX() + ",\"anchorY\":" + anchor.getY() + + ",\"anchorZ\":" + anchor.getZ() + ",\"inTransit\":" + transitTm.inTransitCount() + "}"); return; } @@ -4082,7 +4119,26 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] send(sender, "{\"error\":\"transit not set up\"}"); return; } - transitTm.tick(); + // transit-tick [count] — advance the jump `count` server ticks in ONE round trip. + // + // The count is not a convenience: a flight is only a flight if it is longer than + // ShipTransitManager.DIRECT_CROSSING_MAX_TICKS, so every test of the hyperspace path now + // has to drive at least that many ticks, and one probe call per tick makes a 200-tick + // flight 200 round trips. It repeats the SAME tick — it does not change what a tick does. + int ticksToRun = args.length >= 2 ? Math.max(1, Math.min(2000, parseIntOr(args[1], 1))) : 1; + for (int t = 0; t < ticksToRun; t++) { + transitTm.tick(); + if (transitStack != null) { + transitStack.cellCrossings.tick(); + } + } + // Both mechanisms are advanced above. A jump short enough is performed as a single + // cell-to-cell crossing rather than flown, and its settle is driven by the crossing + // controller, not by the transit map. Ticking only one of them would make "advance the + // jump" mean different things depending on which mechanism the speed selected — and the + // arrival acceptance is meant to be SHARED between them, not written twice. + int crossing = transitStack != null && transitDurableId != null + && transitStack.cellCrossings.isCarrying(transitDurableId) ? 1 : 0; int inTransit = transitTm.inTransitCount(); int targetDim = -1; if (inTransit == 0 && transitMgr.isLoaded(transitTarget)) { @@ -4125,6 +4181,11 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] // the shared parking world. A crew-side test compares the CLIENT's dimension against these // rather than hardcoding an id that is minted per boot. send(sender, "{\"ok\":true,\"inTransit\":" + inTransit + ",\"targetDim\":" + targetDim + // Which mechanism is actually running, emitted in every state so "neither" is a + // pair of zeros rather than a missing field: `inTransit` is the hyperspace flight, + // `crossing` is the direct cell-to-cell settle. A test that wants to know WHICH + // one its speed selected reads these instead of inferring it from timing. + + ",\"crossing\":" + crossing + ",\"poseX\":" + (long) pose[0] + ",\"poseY\":" + (long) pose[1] + ",\"poseZ\":" + (long) pose[2] + ",\"shipY\":" + shipY + ",\"poseDist\":" + poseDist @@ -4531,8 +4592,8 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] // real ship. The ship's LIVE pose is used, never the ledger's: past the face the ledger's copy // is saturated, so a lookup from it would miss the ship by the whole overshoot. if (args.length >= 2 && "seam-carry".equalsIgnoreCase(args[0])) { - zmaster587.advancedRocketry.space.CellSeamController seamCtl = - zmaster587.advancedRocketry.space.SpaceSubsystem.seam(); + zmaster587.advancedRocketry.space.CellCrossingController seamCtl = + zmaster587.advancedRocketry.space.SpaceSubsystem.cellCrossings(); zmaster587.advancedRocketry.space.ShipLedger seamLedger = zmaster587.advancedRocketry.space.SpaceSubsystem.ledger(); if (seamCtl == null || seamLedger == null) { diff --git a/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java b/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java index 1802a1d87..8cd3cc335 100644 --- a/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java +++ b/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java @@ -15,6 +15,7 @@ import zmaster587.advancedRocketry.integration.vs.VSIntegration; import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.space.ShipLedger; +import zmaster587.advancedRocketry.space.ShipTransitManager; import zmaster587.advancedRocketry.space.SpaceSubsystem; import zmaster587.advancedRocketry.tile.TileNavigationComputer; @@ -141,6 +142,25 @@ public long plannedTransitTicks() { plannedSpeed()); } + /** + * Would this jump be performed as a single crossing rather than flown through hyperspace? + * + *

    Asked of {@link ShipTransitManager#isDirectCrossing} — the same predicate the departure reads, + * never a second copy of the rule. A console that quoted one mechanism while the drive performed + * the other would be showing the pilot a flight he is not going to get, and he has no way to + * check.

    + */ + public boolean plannedJumpIsDirect() { + GalacticCoord target = target(); + GalacticCoord origin = currentCoord(); + if (target == null || origin == null) { + return false; + } + return ShipTransitManager.isDirectCrossing( + SpaceSubsystem.frames().distanceBetween(origin, target, SpaceSubsystem.spaceClock()), + plannedSpeed()); + } + /** Where the ship is now, as the durable ledger records it, or {@code null}. */ public GalacticCoord currentCoord() { ShipLedger ledger = SpaceSubsystem.ledger(); diff --git a/src/main/java/zmaster587/advancedRocketry/space/CellCrossingController.java b/src/main/java/zmaster587/advancedRocketry/space/CellCrossingController.java new file mode 100644 index 000000000..35b912fba --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/space/CellCrossingController.java @@ -0,0 +1,266 @@ +package zmaster587.advancedRocketry.space; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.LongSupplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import net.minecraft.util.math.BlockPos; + +/** + * Moves a settled ship from one cell into another in a SINGLE crossing. + * + *

    Two things ask for that, and they differ only in how the destination is chosen:

    + * + *
      + *
    • A seam carry — the ship flew out through its cell's face, and the neighbour it left + * through is where it belongs ({@link #requestCarry}). Before this existed, a ship past its cell + * face was neither stopped nor carried: its pose kept going while the ledger report SATURATED at + * the boundary, so the ship was in one place and named in another. Everything keyed on the name + * then answered about the wrong cell — it could not descend (the named cell holds no bodies), its + * jumps were refused, and the cell it was really in lost the ledger's garbage-collection + * protection.
    • + *
    • A short jump — the drive was fired at a destination the ship reaches in less time than + * the flight would take to present itself ({@link #requestDirectJump}). Routing that through + * hyperspace is pure overhead: two crossings and a park for a flight that is over before it + * starts. {@link ShipTransitManager} owns the decision; this owns the move.
    • + *
    + * + *

    The arithmetic of the seam — when a pose counts as having left, and where in the neighbour the + * ship belongs — is {@link CellSeam}'s, and has no Minecraft in it. What lives here is the world half: + * acquiring the destination cell, capturing the crew, driving the shared {@link ShipCrossingService}, + * and the refcount handoff.

    + * + *

    The handoff order, and why it is not the other one

    + * + *

    The destination is materialized before the source is released. The reverse order leaves a + * window in which the ship holds no cell at all, and a garbage collection landing in that window + * collects the very cell the ship is being pasted into. The cost of this order is that a refused + * crossing must hand the destination back, which is what the failure paths below do.

    + * + *

    A refusal is a normal outcome, not an error: the pool can be full. A refused seam carry keeps + * flying with its report saturated at the boundary — the old behaviour, now the fallback rather than + * the rule — and is retried after a cooldown. A refused jump is reported to its caller, which has + * already charged the pilot for the attempt.

    + */ +public final class CellCrossingController { + + private static final Logger LOGGER = LogManager.getLogger("advancedrocketry/space"); + + /** Ticks before a refused seam carry may be attempted again. */ + private static final int RETRY_COOLDOWN_TICKS = 100; + + /** The arrival paste band in the destination slot world — the entry crossing's geometry, because + * it is the same kind of destination: an empty slot world with nothing at its origin. */ + private static final int SEAM_PASTE_Z = -1024; + private static final int SEAM_PASTE_Y = 200; + private static final int SEAM_LANE_STRIDE = 64; + private static final int SEAM_LANE_COUNT = 8; + + /** + * What the crew is told, and what the log calls the move. The two callers differ in nothing else, + * and a crossing that reported "carried into the next neighbourhood" for a jump across a system + * would be lying to the only person who can see it. + */ + private enum Kind { + SEAM("cell-seam carry", "msg.shipseam.arrived", "msg.shipseam.failed"), + JUMP("direct jump", "msg.shiptransit.arrived", "msg.shiptransit.directfailed"); + + final String label; + final String arrivedKey; + final String failedKey; + + Kind(String label, String arrivedKey, String failedKey) { + this.label = label; + this.arrivedKey = arrivedKey; + this.failedKey = failedKey; + } + } + + private final SpaceManager space; + private final ShipLedger ledger; + private final ShipCrossingService crossing; + private final LongSupplier clock; + private final Map retryAfter = new HashMap<>(); + private int laneCounter; + + public CellCrossingController(SpaceManager space, ShipLedger ledger, ShipCrossingService.Ops ops, + LongSupplier clock) { + this.space = space; + this.ledger = ledger; + this.crossing = new ShipCrossingService(ops); + this.clock = clock; + } + + /** + * Carry the SETTLED ship at {@code afcPos} out of {@code cell} and into the neighbour its pose has + * left through. Returns {@code true} when the crossing started, in which case the ship has been + * cut out of this world and the caller must stop touching it this tick. + * + *

    {@code shipPos} is passed in rather than re-read: the decision and the arrival must be + * computed from the SAME pose. Re-reading it here would let a fast ship be judged on one position + * and placed by another, and at these speeds the two can be thousands of blocks apart.

    + */ + public boolean requestCarry(int slotDim, BlockPos afcPos, UUID shipId, GalacticCoord cell, + double[] shipPos) { + if (shipId == null || cell == null || shipPos == null || crossing.isCrossing(shipId)) { + return false; + } + if (!isSettled(shipId)) { + // Only a ship genuinely settled in a cell can leave one by flying. A ship mid-arrival sits + // in the paste band, which is far outside its cell's pose range and would otherwise read as + // an escape on every single crossing. + return false; + } + if (!CellSeam.shouldCarry(shipPos[0], shipPos[1], shipPos[2])) { + return false; + } + long now = clock.getAsLong(); + Long cooldown = retryAfter.get(shipId); + if (cooldown != null && now < cooldown) { + return false; + } + GalacticCoord destCoord = CellSeam.carriedCoord(cell, shipPos[0], shipPos[1], shipPos[2]); + return cross(slotDim, afcPos, shipId, ledger.get(shipId).coord, destCoord, shipPos, Kind.SEAM); + } + + /** + * Cross the SETTLED ship at {@code afcPos} from {@code cell} straight into {@code target}, with no + * hyperspace leg. Returns {@code true} when the crossing started. + * + *

    Unlike a seam carry this has no cooldown and no refusal fallback: the caller has already + * charged the drive for the attempt, so a {@code false} here is a failed jump that must be + * reported, not a condition to be retried quietly next tick.

    + * + *

    It also READS the ship's pose rather than being handed one, which the seam may not do. The + * seam's decision is about the pose — judged on one position and placed by another, a fast + * ship lands thousands of blocks from where it was measured — while a jump's destination comes from + * the pilot's target and does not depend on where in the cell the ship happens to be.

    + */ + public boolean requestDirectJump(int slotDim, BlockPos afcPos, UUID shipId, GalacticCoord cell, + GalacticCoord target) { + if (shipId == null || cell == null || target == null || crossing.isCrossing(shipId)) { + return false; + } + if (!isSettled(shipId)) { + return false; + } + double[] shipPos = crossing.ops().shipWorldPosition(slotDim, afcPos); + if (shipPos == null) { + LOGGER.warn("[SPACE] direct jump refused for ship {}: no ship resolves at {} in slot {}", + shipId, afcPos, slotDim); + return false; + } + return cross(slotDim, afcPos, shipId, cell, target, shipPos, Kind.JUMP); + } + + /** Whether the ledger has this ship SETTLED somewhere — the precondition both entries share. */ + private boolean isSettled(UUID shipId) { + ShipLedger.Entry entry = ledger.get(shipId); + return entry != null && entry.state == ShipLedger.State.SETTLED; + } + + /** The move itself: acquire the destination, capture, cut, release the source, name the result. */ + private boolean cross(int slotDim, BlockPos afcPos, UUID shipId, GalacticCoord sourceCell, + GalacticCoord destCoord, double[] shipPos, Kind kind) { + long now = clock.getAsLong(); + final int destSlotDim; + try { + destSlotDim = space.materialize(destCoord); + } catch (SpaceManager.PoolExhaustedException full) { + // No slot for the destination. The ship stays where it is. The crew is only READ here, so + // nobody is dismounted by a refusal. + List told = crossing.ops().peekCrew(slotDim, afcPos, shipPos); + LOGGER.warn("[SPACE] {} refused for ship {} leaving {}: {} (told {} aboard)", + kind.label, shipId, sourceCell.cellKey(), full.getMessage(), + told == null ? 0 : told.size()); + crossing.ops().messageCrew(told, "msg.shipseam.refused"); + if (kind == Kind.SEAM) { + retryAfter.put(shipId, now + RETRY_COOLDOWN_TICKS); + } + return false; + } + + // Capture only now, with the destination GRANTED — the last refusal is behind — and still + // before the cut: the crossing cuts the seat blocks, and a post-cut capture finds nothing. + final List crew = crossing.ops().captureCrew(slotDim, afcPos, shipPos); + + int lane = (laneCounter++ % SEAM_LANE_COUNT); + double[] pose = CellWorldMapper.poseWorldOf(destCoord); + final GalacticCoord arrivalCoord = destCoord; + final Kind arrivalKind = kind; + BlockPos anchor = crossing.begin(shipId, slotDim, shipPos, destSlotDim, + lane * SEAM_LANE_STRIDE, SEAM_PASTE_Y, SEAM_PASTE_Z, crew, pose, + new ShipCrossingService.Completion() { + @Override + public void settled(UUID id) { + ledger.settle(id, arrivalCoord); + crossing.ops().messageCrew(crew, arrivalKind.arrivedKey); + LOGGER.info("[SPACE] {} settled: ship {} now in cell {} (slot {})", + arrivalKind.label, id, arrivalCoord.cellKey(), destSlotDim); + } + + @Override + public void abandoned(UUID id) { + // The arrival never finished. The ship is somewhere in the destination slot + // world — which place depends on the half that stalled, and the crossing's own + // give-up line names it; do not claim one here. Settle it in the destination + // anyway: that IS the cell it is in, and leaving the row IN_TRANSIT would strand + // a real ship in a state nothing else advances. + ledger.settle(id, arrivalCoord); + crossing.ops().messageCrew(crew, arrivalKind.failedKey); + LOGGER.error("[SPACE] {} settle never completed for ship {} arriving in " + + "cell {} (slot {}) - see the crossing give-up line above for which " + + "half stalled", arrivalKind.label, id, arrivalCoord.cellKey(), + destSlotDim); + } + }); + if (anchor == null) { + LOGGER.error("[SPACE] {} crossing failed for ship {} leaving cell {}", + kind.label, shipId, sourceCell.cellKey()); + // The cut never produced a paste, so the ship is (best-effort) still intact where it was: + // hand the destination back, re-seat the crew we already captured, and let it keep flying. + space.dematerialize(destCoord); + crossing.ops().reseat(slotDim, + new BlockPos(shipPos[0], shipPos[1], shipPos[2]), crew, shipId, null); + crossing.ops().messageCrew(crew, kind.failedKey); + if (kind == Kind.SEAM) { + retryAfter.put(shipId, now + RETRY_COOLDOWN_TICKS); + } + return false; + } + + // The ship is physically out of the source cell now, so the source is released NOW and not on + // settle — the settle only completes the arrival on the far side. The destination refcount was + // taken above, so the ship is never between cells. + space.markDirty(sourceCell); + space.dematerialize(sourceCell); + space.markDirty(destCoord); + // SETTLED at the destination, from the cut — deliberately NOT `beginTransit`. IN_TRANSIT is + // not a generic "crossing" state: `LoginRestore` reads it as "parked in the shared hyperspace + // world" and resolves the player through the transit dim, so a crossing ship wearing it + // would orphan anyone who logged in during the few ticks of re-assembly. The row names the + // cell the ship's blocks are actually in, which is also the cell whose refcount is held. + // + // For a jump this is also the whole saving: a crossing that never enters IN_TRANSIT has no + // mid-flight for a restart to resume, so it needs no snapshot and cannot strand a ship. + ledger.settle(shipId, destCoord); + LOGGER.info("[SPACE] {} started: ship {} {} -> {} (slot {})", + kind.label, shipId, sourceCell.cellKey(), destCoord.cellKey(), destSlotDim); + return true; + } + + /** Advance every in-flight crossing one tick (the shared crossing settle loop). */ + public void tick() { + crossing.tick(); + } + + /** Whether {@code shipId} is being moved between cells right now — by either entry point. */ + public boolean isCarrying(UUID shipId) { + return crossing.isCrossing(shipId); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/space/CellSeam.java b/src/main/java/zmaster587/advancedRocketry/space/CellSeam.java index 10cced275..6252b5d59 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/CellSeam.java +++ b/src/main/java/zmaster587/advancedRocketry/space/CellSeam.java @@ -6,7 +6,7 @@ *

    A cell's local range is finite, so a ship under sustained thrust reaches its face. Two answers * were possible: stop it, or carry it. This class holds the second one's arithmetic — when a pose * has left its cell far enough to count, and where the ship belongs in the neighbour it entered. - * Nothing here touches Minecraft, a world or a ledger; that is {@link CellSeamController}'s work.

    + * Nothing here touches Minecraft, a world or a ledger; that is {@link CellCrossingController}'s work.

    * *

    Why a margin exists at all

    * diff --git a/src/main/java/zmaster587/advancedRocketry/space/CellSeamController.java b/src/main/java/zmaster587/advancedRocketry/space/CellSeamController.java deleted file mode 100644 index 3b512e3f1..000000000 --- a/src/main/java/zmaster587/advancedRocketry/space/CellSeamController.java +++ /dev/null @@ -1,186 +0,0 @@ -package zmaster587.advancedRocketry.space; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.function.LongSupplier; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import net.minecraft.util.math.BlockPos; - -/** - * Carries a ship that has flown out of its cell into the neighbouring cell it left through. - * - *

    Before this existed, a ship past its cell face was neither stopped nor carried: its pose kept - * going while the ledger report SATURATED at the boundary, so the ship was in one place and named in - * another. Everything keyed on the name then answered about the wrong cell — it could not descend - * (the named cell holds no bodies), its jumps were refused, and the cell it was really in lost the - * ledger's garbage-collection protection.

    - * - *

    The arithmetic — when a pose counts as having left, and where in the neighbour the ship belongs - * — is {@link CellSeam}'s, and has no Minecraft in it. What lives here is the world half: acquiring - * the destination cell, capturing the crew, driving the shared {@link ShipCrossingService}, and the - * refcount handoff.

    - * - *

    The handoff order, and why it is not the other one

    - * - *

    The destination is materialized before the source is released. The reverse order leaves a - * window in which the ship holds no cell at all, and a garbage collection landing in that window - * collects the very cell the ship is being pasted into. The cost of this order is that a refused - * carry must hand the destination back, which is what the failure paths below do.

    - * - *

    A refusal is a normal outcome, not an error: the pool can be full. A refused ship keeps flying - * with its report saturated at the boundary — the old behaviour, now the fallback rather than the - * rule — and the carry is retried after a cooldown.

    - */ -public final class CellSeamController { - - private static final Logger LOGGER = LogManager.getLogger("advancedrocketry/space"); - - /** Ticks before a refused carry may be attempted again. */ - private static final int RETRY_COOLDOWN_TICKS = 100; - - /** The arrival paste band in the destination slot world — the entry crossing's geometry, because - * it is the same kind of destination: an empty slot world with nothing at its origin. */ - private static final int SEAM_PASTE_Z = -1024; - private static final int SEAM_PASTE_Y = 200; - private static final int SEAM_LANE_STRIDE = 64; - private static final int SEAM_LANE_COUNT = 8; - - private final SpaceManager space; - private final ShipLedger ledger; - private final ShipCrossingService crossing; - private final LongSupplier clock; - private final Map retryAfter = new HashMap<>(); - private int laneCounter; - - public CellSeamController(SpaceManager space, ShipLedger ledger, ShipCrossingService.Ops ops, - LongSupplier clock) { - this.space = space; - this.ledger = ledger; - this.crossing = new ShipCrossingService(ops); - this.clock = clock; - } - - /** - * Carry the SETTLED ship at {@code afcPos} out of {@code cell} and into the neighbour its pose has - * left through. Returns {@code true} when the crossing started, in which case the ship has been - * cut out of this world and the caller must stop touching it this tick. - * - *

    {@code shipPos} is passed in rather than re-read: the decision and the arrival must be - * computed from the SAME pose. Re-reading it here would let a fast ship be judged on one position - * and placed by another, and at these speeds the two can be thousands of blocks apart.

    - */ - public boolean requestCarry(int slotDim, BlockPos afcPos, UUID shipId, GalacticCoord cell, - double[] shipPos) { - if (shipId == null || cell == null || shipPos == null || crossing.isCrossing(shipId)) { - return false; - } - ShipLedger.Entry entry = ledger.get(shipId); - if (entry == null || entry.state != ShipLedger.State.SETTLED) { - // Only a ship genuinely settled in a cell can leave one by flying. A ship mid-arrival sits - // in the paste band, which is far outside its cell's pose range and would otherwise read as - // an escape on every single crossing. - return false; - } - if (!CellSeam.shouldCarry(shipPos[0], shipPos[1], shipPos[2])) { - return false; - } - long now = clock.getAsLong(); - Long cooldown = retryAfter.get(shipId); - if (cooldown != null && now < cooldown) { - return false; - } - - final GalacticCoord sourceCell = entry.coord; - final GalacticCoord destCoord = CellSeam.carriedCoord(cell, shipPos[0], shipPos[1], shipPos[2]); - - final int destSlotDim; - try { - destSlotDim = space.materialize(destCoord); - } catch (SpaceManager.PoolExhaustedException full) { - // No slot for the neighbour. The ship stays where it is, keeps flying, and keeps reporting - // saturated at the face — wrong by the overshoot, but pointing at a cell that exists. The - // crew is only READ here, so nobody is dismounted by a refusal. - List told = crossing.ops().peekCrew(slotDim, afcPos, shipPos); - LOGGER.warn("[SPACE] cell-seam carry refused for ship {} leaving {}: {} (told {} aboard)", - shipId, sourceCell.cellKey(), full.getMessage(), told == null ? 0 : told.size()); - crossing.ops().messageCrew(told, "msg.shipseam.refused"); - retryAfter.put(shipId, now + RETRY_COOLDOWN_TICKS); - return false; - } - - // Capture only now, with the destination GRANTED — the last refusal is behind — and still - // before the cut: the crossing cuts the seat blocks, and a post-cut capture finds nothing. - final List crew = crossing.ops().captureCrew(slotDim, afcPos, shipPos); - - int lane = (laneCounter++ % SEAM_LANE_COUNT); - double[] pose = CellWorldMapper.poseWorldOf(destCoord); - BlockPos anchor = crossing.begin(shipId, slotDim, shipPos, destSlotDim, - lane * SEAM_LANE_STRIDE, SEAM_PASTE_Y, SEAM_PASTE_Z, crew, pose, - new ShipCrossingService.Completion() { - @Override - public void settled(UUID id) { - ledger.settle(id, destCoord); - crossing.ops().messageCrew(crew, "msg.shipseam.arrived"); - LOGGER.info("[SPACE] cell-seam carry settled: ship {} now in cell {} (slot {})", - id, destCoord.cellKey(), destSlotDim); - } - - @Override - public void abandoned(UUID id) { - // The arrival never finished. The ship is somewhere in the destination slot - // world — which place depends on the half that stalled, and the crossing's own - // give-up line names it; do not claim one here. Settle it in the destination - // anyway: that IS the cell it is in, and leaving the row IN_TRANSIT would strand - // a real ship in a state nothing else advances. - ledger.settle(id, destCoord); - crossing.ops().messageCrew(crew, "msg.shipseam.failed"); - LOGGER.error("[SPACE] cell-seam settle never completed for ship {} arriving in " - + "cell {} (slot {}) - see the crossing give-up line above for which " - + "half stalled", id, destCoord.cellKey(), destSlotDim); - } - }); - if (anchor == null) { - LOGGER.error("[SPACE] cell-seam crossing failed for ship {} leaving cell {}", - shipId, sourceCell.cellKey()); - // The cut never produced a paste, so the ship is (best-effort) still intact where it was: - // hand the destination back, re-seat the crew we already captured, and let it keep flying. - space.dematerialize(destCoord); - crossing.ops().reseat(slotDim, - new BlockPos(shipPos[0], shipPos[1], shipPos[2]), crew, shipId, null); - crossing.ops().messageCrew(crew, "msg.shipseam.failed"); - retryAfter.put(shipId, now + RETRY_COOLDOWN_TICKS); - return false; - } - - // The ship is physically out of the source cell now, so the source is released NOW and not on - // settle — the settle only completes the arrival on the far side. The destination refcount was - // taken above, so the ship is never between cells. - space.markDirty(sourceCell); - space.dematerialize(sourceCell); - space.markDirty(destCoord); - // SETTLED at the destination, from the cut — deliberately NOT `beginTransit`. IN_TRANSIT is - // not a generic "crossing" state: `LoginRestore` reads it as "parked in the shared hyperspace - // world" and resolves the player through the transit dim, so a seam-crossing ship wearing it - // would orphan anyone who logged in during the few ticks of re-assembly. The row names the - // cell the ship's blocks are actually in, which is also the cell whose refcount is held. - ledger.settle(shipId, destCoord); - LOGGER.info("[SPACE] cell-seam carry started: ship {} {} -> {} (slot {})", - shipId, sourceCell.cellKey(), destCoord.cellKey(), destSlotDim); - return true; - } - - /** Advance every in-flight seam carry one tick (the shared crossing settle loop). */ - public void tick() { - crossing.tick(); - } - - /** Whether {@code shipId} is being carried across a cell face right now. */ - public boolean isCarrying(UUID shipId) { - return crossing.isCrossing(shipId); - } -} diff --git a/src/main/java/zmaster587/advancedRocketry/space/ShipTransitManager.java b/src/main/java/zmaster587/advancedRocketry/space/ShipTransitManager.java index adcfcfd39..9d97bc8c4 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/ShipTransitManager.java +++ b/src/main/java/zmaster587/advancedRocketry/space/ShipTransitManager.java @@ -378,6 +378,8 @@ private static final class PendingReseat { private final LongSupplier clock; /** Offline-progress gate; {@code null} = always advance (state-machine unit tests). */ private OfflineProgress offlineProgress; + /** Performs a jump short enough to skip hyperspace; {@code null} = none wired, see the branch. */ + private DirectCrosser directCrosser; /** Arrival placement policy; {@code null} = arrive exactly on the aimed coordinate. */ private ArrivalPlacement arrivalPlacement; /** @@ -421,6 +423,39 @@ public boolean beginTransit(String shipId, GalacticCoord origin, int originSlotD if (transits.containsKey(shipId)) { return false; // already in transit } + long speed = Math.max(1L, speedBlocksPerTick); + long now = clock.getAsLong(); + // The flight is priced ONCE, here, through both cells' frames as they stand at departure. + // A jump is a commitment: the pilot saw a forecast at the console and the drive spent its + // burst against it, so re-pricing mid-flight because the destination kept orbiting would + // charge him for a decision he could not have made differently. + // + // It is also read BEFORE anything is allocated or cut, because the price is what chooses the + // mechanism: a leg short enough to be over before it presents itself is performed as one + // crossing instead (see DIRECT_CROSSING_MAX_TICKS). Everything the hyperspace path sets up — + // the lane, the crew capture, the floor snapshot — is work the direct path must not do. + double distance = (frames == null ? CellFrames.STATIC : frames) + .distanceBetween(origin, target, now); + if (isDirectCrossing(distance, speed)) { + if (directCrosser == null) { + // Nothing is wired to perform one, so the jump is flown the long way. Said out loud: + // a mechanism that silently does not exist is indistinguishable from one that was not + // chosen, and this branch is exactly where a wiring mistake would hide. + LOGGER.warn("[SPACE] jump for ship {} qualifies as a direct crossing ({} ticks) but no " + + "direct crosser is wired - flying it through hyperspace instead", + shipId, zmaster587.advancedRocketry.hyperdrive.JumpSpeed + .transitTicks(distance, speed)); + } else { + boolean crossed = directCrosser.crossDirect(shipId, origin, originSlotDim, + originAnchor, target); + LOGGER.info("[SPACE] direct crossing {} for ship {} {} -> {} ({} blocks, {} ticks of " + + "flight it does not need)", + crossed ? "began" : "REFUSED", shipId, origin.cellKey(), target.cellKey(), + (long) Math.ceil(distance), zmaster587.advancedRocketry.hyperdrive.JumpSpeed + .transitTicks(distance, speed)); + return crossed; + } + } HyperspaceTiles.Tile tile = tiles.allocate(); // Capture the seated crew BEFORE the depart crossing cuts the seat blocks (a post-cut capture finds // nothing). captureCrew stashes the full crew inside the crosser (keyed by shipId) for the reseat at @@ -455,14 +490,6 @@ public boolean beginTransit(String shipId, GalacticCoord origin, int originSlotD } // Refcount handoff, half 1: the ship has left the origin cell. space.dematerialize(origin); - long speed = Math.max(1L, speedBlocksPerTick); - long now = clock.getAsLong(); - // The flight is priced ONCE, here, through both cells' frames as they stand at departure. - // A jump is a commitment: the pilot saw a forecast at the console and the drive spent its - // burst against it, so re-pricing mid-flight because the destination kept orbiting would - // charge him for a decision he could not have made differently. - double distance = (frames == null ? CellFrames.STATIC : frames) - .distanceBetween(origin, target, now); long distanceBlocks = (long) Math.ceil(distance); // The ETA goes through the same law the console's forecast quotes, so the flight the pilot // was shown is the flight he gets. @@ -910,6 +937,58 @@ public enum Phase { private static final long DEPARTING_TICKS = 60L; private static final long ARRIVING_TICKS = 100L; + /** + * At or below this many ticks a jump is not flown at all — it is performed as a single cell→cell + * crossing, with no hyperspace leg. Derived, not chosen: {@link #phaseOf} reads a flight as + * departing, then cruising, then arriving, so a flight shorter than the two windows together never + * reports {@code CRUISING} at all. It is leaving, then it is arriving, and there was no flight in + * between. That is the point at which the mechanism's own presentation degenerates, and it is + * therefore the point at which the mechanism should stop being used. + * + *

    Because it is a sum of the two windows rather than a third number beside them, moving either + * window moves this with it. Written down separately, the three would drift.

    + * + *

    The crossing's own cost cannot invert the rule for any value: a hyperspace jump performs the + * crossing TWICE (depart and arrive) plus the spool and the flight, so the comparison is {@code C} + * against {@code spool + 2C + transitTicks} and {@code C} appears on both sides.

    + */ + public static final long DIRECT_CROSSING_MAX_TICKS = DEPARTING_TICKS + ARRIVING_TICKS; + + /** + * Would a jump of {@code distanceBlocks} at {@code speedBlocksPerTick} be performed as a direct + * crossing rather than flown through hyperspace? + * + *

    This is the only place that decides. The pilot's forecast at the console and the + * departure itself both call it, because a jump that is quoted as one mechanism and executed as the + * other is a lie the pilot cannot check. The rule keys on the COMPUTED DURATION and deliberately + * not on the route: with a fast enough drive an interstellar leg is also over in a tick, and a + * route-shaped rule ("in-system is direct") would then be wrong in the interesting case.

    + */ + public static boolean isDirectCrossing(double distanceBlocks, long speedBlocksPerTick) { + return zmaster587.advancedRocketry.hyperdrive.JumpSpeed + .transitTicks(distanceBlocks, speedBlocksPerTick) <= DIRECT_CROSSING_MAX_TICKS; + } + + /** + * Performs a jump short enough not to need hyperspace, as ONE cell→cell crossing. Kept behind + * a seam for the same reason {@link Crosser} is: the branch above must be decidable in a test with + * no world under it. Production is {@link CellCrossingController#requestDirectJump}. + */ + public interface DirectCrosser { + /** + * Cut the ship named {@code shipId} out of {@code origin} (slot {@code originSlotDim}, anchor + * {@code originAnchor}) and paste it into {@code target}, settling the ledger straight there. + * {@code false} = the crossing did not start, and the caller reports a failed jump. + */ + boolean crossDirect(String shipId, GalacticCoord origin, int originSlotDim, + BlockPos originAnchor, GalacticCoord target); + } + + /** Install the direct-crossing seam. {@code null} means short jumps fly through hyperspace and say so. */ + public void setDirectCrosser(DirectCrosser crosser) { + this.directCrosser = crosser; + } + /** Install the offline-progress gate (config mode + online check). {@code null} restores always-advance. */ public void setOfflineProgress(OfflineProgress policy) { this.offlineProgress = policy; diff --git a/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystem.java b/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystem.java index acc8916ea..076d12625 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystem.java +++ b/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystem.java @@ -67,7 +67,7 @@ public final class SpaceSubsystem { public final ShipTransitManager transit; public final ShipEntryController entry; public final DescentController descent; - public final CellSeamController seam; + public final CellCrossingController cellCrossings; private int gcTickCounter; /** Set by the pool-pressure eviction listener; consumed on the next server tick to run an extra GC. */ private boolean pressureGcRequested; @@ -118,8 +118,26 @@ public SpaceSubsystem(SlotBinder binder, java.util.function.LongSupplier clock, SpaceSubsystem::launchBodyAddress, useClock); this.descent = new DescentController(this.manager, this.ledger, new VSShipCrossingOps(), new VSDescentPasteResolver(), useClock); - this.seam = new CellSeamController(this.manager, this.ledger, new VSShipCrossingOps(), + this.cellCrossings = new CellCrossingController(this.manager, this.ledger, new VSShipCrossingOps(), useClock); + // A jump too short to be worth a hyperspace leg is performed by the same machinery that carries + // a ship across a cell face — one crossing, ledger straight to the destination, no lane and no + // mid-flight. The transit manager decides WHICH jumps those are; this hands it the means. + this.transit.setDirectCrosser((shipId, origin, originSlotDim, originAnchor, target) -> { + // The transit manager keys ships by STRING, the ledger and the crossing by UUID. Not every + // string is one: a fixture may depart under a synthetic name, and a crossing cannot look + // that up. Refuse it here rather than throw out of a departure the pilot has paid for. + java.util.UUID durableId; + try { + durableId = java.util.UUID.fromString(shipId); + } catch (IllegalArgumentException notADurableId) { + AdvancedRocketry.logger.warn("[SPACE] direct crossing refused for ship '{}': it is not " + + "a durable id, so nothing can resolve it in the ledger", shipId); + return false; + } + return this.cellCrossings.requestDirectJump(originSlotDim, originAnchor, durableId, + origin, target); + }); } /** The live subsystem, or {@code null} when none is attached (before server start, or on a client). */ @@ -212,9 +230,10 @@ public static ShipEntryController entry() { return current == null ? null : current.entry; } - /** The live cell-seam controller, or {@code null} when no subsystem is attached. */ - public static CellSeamController seam() { - return current == null ? null : current.seam; + /** The live cell-to-cell crossing controller (seam carries AND short jumps), or {@code null} + * when no subsystem is attached. */ + public static CellCrossingController cellCrossings() { + return current == null ? null : current.cellCrossings; } /** The live descent controller, or {@code null} when no subsystem is attached. */ diff --git a/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystemEvents.java b/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystemEvents.java index 6364fa05f..c4f7ec1e8 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystemEvents.java +++ b/src/main/java/zmaster587/advancedRocketry/space/SpaceSubsystemEvents.java @@ -73,7 +73,7 @@ public void onServerTick(TickEvent.ServerTickEvent event) { // Advance in-flight DESCENTS (the inverse crossing, same async re-seat + settle). live.descent.tick(); // Advance in-flight CELL-SEAM carries (a ship that flew out of its cell into the next one). - live.seam.tick(); + live.cellCrossings.tick(); // Rebroadcast the per-slot render bodies (throttled) so the slot-world sky (BoundarySky) // tracks each settled ship's direction to the bodies of its cell. SystemBodiesProducer.onBroadcastTick(FMLCommonHandler.instance().getMinecraftServerInstance()); diff --git a/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java b/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java index d2dee1d72..3ec1c338d 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java @@ -505,8 +505,8 @@ public void update() { // the carry is asked BEFORE the position is reported, because a report that // saturates is what a ship gets when the carry was refused, not what it gets while // one is available. - zmaster587.advancedRocketry.space.CellSeamController seamCtl = - zmaster587.advancedRocketry.space.SpaceSubsystem.seam(); + zmaster587.advancedRocketry.space.CellCrossingController seamCtl = + zmaster587.advancedRocketry.space.SpaceSubsystem.cellCrossings(); if (seamCtl != null && seamCtl.requestCarry(world.provider.getDimension(), getPos(), shipId, cell, pose)) { return; diff --git a/src/main/resources/assets/advancedrocketry/lang/en_US.lang b/src/main/resources/assets/advancedrocketry/lang/en_US.lang index 0d8b18901..8a7599eb1 100644 --- a/src/main/resources/assets/advancedrocketry/lang/en_US.lang +++ b/src/main/resources/assets/advancedrocketry/lang/en_US.lang @@ -920,6 +920,7 @@ msg.shipseam.failed=§cThe crossing failed - the ship could not be carried into msg.shipseam.arrived=§aThe ship has crossed into the next neighbourhood. msg.shiptransit.departed=§aJump engaged - the ship is under way. Helm control is offline until you arrive. msg.shiptransit.arrived=§aArrived - the ship is back in normal space. Helm control is yours again. +msg.shiptransit.directfailed=§cThe crossing failed - your ship is at its destination but not cleanly placed. It is safe; report this. msg.shiptransit.arrivalrecovered=§eThe jump completed, but not cleanly - your ship is at its arrival point rather than on course. It is safe; report this. msg.shiptransit.arrivalstalled=§cThe jump cannot finish right now. Your ship is not lost - it stays in transit and will arrive. Report this. diff --git a/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang b/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang index 9fda3f153..859323e53 100644 --- a/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang +++ b/src/main/resources/assets/advancedrocketry/lang/ru_RU.lang @@ -521,6 +521,7 @@ msg.shipseam.failed=§cПереход не удался — корабль не msg.shipseam.arrived=§aКорабль перешёл в соседнюю окрестность. msg.shiptransit.departed=§aПрыжок начат — корабль в пути. Управление отключено до прибытия. msg.shiptransit.arrived=§aПрибытие — корабль снова в обычном пространстве. Управление снова ваше. +msg.shiptransit.directfailed=§cПереход не удался — корабль в точке назначения, но размещён не чисто. Он цел; сообщите об этом. msg.shiptransit.arrivalrecovered=§eПрыжок завершён, но не чисто — корабль стоит в точке прибытия, а не на курсе. Он цел; сообщите об этом. msg.shiptransit.arrivalstalled=§cПрыжок сейчас не может завершиться. Корабль не потерян — он остаётся в полёте и прибудет. Сообщите об этом. diff --git a/src/test/java/zmaster587/advancedRocketry/test/AdvancedRocketryTestConstants.java b/src/test/java/zmaster587/advancedRocketry/test/AdvancedRocketryTestConstants.java index d06db32a5..2b153082b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/AdvancedRocketryTestConstants.java +++ b/src/test/java/zmaster587/advancedRocketry/test/AdvancedRocketryTestConstants.java @@ -1,5 +1,9 @@ package zmaster587.advancedRocketry.test; +import zmaster587.advancedRocketry.space.CellFrames; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.space.ShipTransitManager; + /** * Shared constants for AR test fixtures. Keep values stable across runs so * snapshot/round-trip assertions stay deterministic. @@ -14,6 +18,40 @@ public final class AdvancedRocketryTestConstants { /** Deterministic world seed for any worldgen scenario. */ public static final long DETERMINISTIC_WORLD_SEED = 0x4151544553544CL; // "AQTESTL" + /** + * How far apart the space fixtures put their two cells: one sector. + * {@code artest space transit-setup*} builds origin and target one sector apart, and it is the only + * distance a fixture jump is ever priced over. + * + *

    MEASURED through the same law the departure prices a jump with, never written down. It was + * written down once, as 4M, from a probe comment that predated the cell growing to 32M — and the + * speeds derived from it put a "hyperspace" fixture 2 560 ticks from its destination.

    + */ + public static final long FIXTURE_CELL_SPACING_BLOCKS = (long) Math.ceil( + CellFrames.STATIC.distanceBetween( + GalacticCoord.ofSectorLocal(0L, 0L, 0L, 0L, 0L, 0L), + GalacticCoord.ofSectorLocal(1L, 0L, 0L, 0L, 0L, 0L), 0L)); + + /** + * A jump speed that gives a REAL hyperspace flight over {@link #FIXTURE_CELL_SPACING_BLOCKS} — + * with a lane, a park and a mid-flight a stimulus can land inside. + * + *

    Derived from the rule that chooses the mechanism rather than written down beside it: a jump + * of at most {@link ShipTransitManager#DIRECT_CROSSING_MAX_TICKS} ticks is performed as a single + * crossing instead, so a fixture that means to test hyperspace must be slower than that — but only + * just. Every tick of the flight is a tick some test has to drive, so the margin is ten ticks and + * not a factor: a comfortable factor of two would double the cost of every hyperspace e2e in the + * suite for no coverage at all.

    + */ + public static final long HYPERSPACE_JUMP_SPEED = + FIXTURE_CELL_SPACING_BLOCKS / (ShipTransitManager.DIRECT_CROSSING_MAX_TICKS + 10L); + + /** + * A jump speed that makes the same distance a DIRECT cell→cell crossing: one tick of flight, + * so the rule fires and no hyperspace lane is ever allocated. + */ + public static final long DIRECT_JUMP_SPEED = FIXTURE_CELL_SPACING_BLOCKS; + /** Stable dimension ids the test fixtures assume. */ public static final int TEST_PLANET_EARTHLIKE_DIM = 9001; public static final int TEST_PLANET_VACUUM_DIM = 9002; diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/VSFlightSmoothnessAcrossJumpE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/VSFlightSmoothnessAcrossJumpE2ETest.java index bf9260b3c..d04bbed76 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/VSFlightSmoothnessAcrossJumpE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/VSFlightSmoothnessAcrossJumpE2ETest.java @@ -11,6 +11,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static zmaster587.advancedRocketry.test.AdvancedRocketryTestConstants.HYPERSPACE_JUMP_SPEED; import static org.junit.Assert.assertTrue; /** @@ -196,7 +197,8 @@ public void aShipFliesAsSmoothlyAfterAJumpAsBeforeOne() throws Exception { String begin = exec("artest space transit-begin " + originDim + " " + (int) Math.round(readDouble(shipNow, "posX")) + " " + (int) Math.round(readDouble(shipNow, "posY")) - + " " + (int) Math.round(readDouble(shipNow, "posZ"))); + + " " + (int) Math.round(readDouble(shipNow, "posZ")) + + " " + HYPERSPACE_JUMP_SPEED); assertTrue("ARRANGEMENT: the transit must begin (departure crossing): " + begin, readBool(begin, "began")); @@ -204,7 +206,7 @@ public void aShipFliesAsSmoothlyAfterAJumpAsBeforeOne() throws Exception { String lastTick = ""; int arriveBudget = (int) (120 * TestTimeouts.factor()); for (int i = 0; i < arriveBudget && targetDim < 0; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readIntOr(lastTick, "inTransit", -1) == 0) { targetDim = readIntOr(lastTick, "targetDim", -1); break; @@ -218,7 +220,7 @@ public void aShipFliesAsSmoothlyAfterAJumpAsBeforeOne() throws Exception { int reseatBudget = (int) (60 * TestTimeouts.factor()); String lastReseatTick = ""; for (int i = 0; i < reseatBudget && !seatedOnArrival; i++) { - lastReseatTick = exec("artest space transit-tick"); + lastReseatTick = exec("artest space transit-tick 10"); bot().waitTicks(2); seatedOnArrival = bot().reportRidingEntity().get("riding").getAsBoolean() && bot().reportWeather().get("dim").getAsInt() == targetDim; diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/VSMidTransitRelogControlE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/VSMidTransitRelogControlE2ETest.java index a8786d5c9..bcfe8ecb7 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/VSMidTransitRelogControlE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/VSMidTransitRelogControlE2ETest.java @@ -14,6 +14,7 @@ import zmaster587.advancedRocketry.space.GalacticCoord; +import static zmaster587.advancedRocketry.test.AdvancedRocketryTestConstants.HYPERSPACE_JUMP_SPEED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -150,9 +151,9 @@ public void aPilotWhoRelogsMidTransitRegainsControlOnArrival() throws Exception // ticks (the cells sit one 4M-block sector apart), so the relog lands INSIDE the transit // instead of racing a single-tick jump. --------------------------------------------------- String begin = exec("artest space transit-begin " + originDim - + " " + ax + " " + ay + " " + az + " 100000"); + + " " + ax + " " + ay + " " + az + " " + HYPERSPACE_JUMP_SPEED); assertTrue("the transit must begin (departure crossing): " + begin, readBool(begin, "began")); - String firstTick = exec("artest space transit-tick"); + String firstTick = exec("artest space transit-tick 10"); assertTrue("the ship must actually be IN TRANSIT when the pilot relogs — otherwise this " + "pins an ordinary relog, not the mid-transit one: " + firstTick, readInt(firstTick, "inTransit") >= 1); @@ -167,7 +168,7 @@ public void aPilotWhoRelogsMidTransitRegainsControlOnArrival() throws Exception String lastTick = ""; int arriveBudget = (int) (80 * TestTimeouts.factor()); for (int i = 0; i < arriveBudget && targetDim < 0; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { targetDim = readInt(lastTick, "targetDim"); break; @@ -183,7 +184,7 @@ public void aPilotWhoRelogsMidTransitRegainsControlOnArrival() throws Exception int reseatBudget = (int) (60 * TestTimeouts.factor()); String lastReseatTick = ""; for (int i = 0; i < reseatBudget && !seatedOnArrival; i++) { - lastReseatTick = exec("artest space transit-tick"); + lastReseatTick = exec("artest space transit-tick 10"); bot().waitTicks(2); seatedOnArrival = bot().reportRidingEntity().get("riding").getAsBoolean() && bot().reportWeather().get("dim").getAsInt() == targetDim; diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/VSTransitCrewGroupE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/VSTransitCrewGroupE2ETest.java index bee85411b..e602f7d23 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/VSTransitCrewGroupE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/VSTransitCrewGroupE2ETest.java @@ -9,6 +9,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static zmaster587.advancedRocketry.test.AdvancedRocketryTestConstants.HYPERSPACE_JUMP_SPEED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -143,7 +144,7 @@ private static long gameSeen(String traceJson) { } /** Blocks per tick for the jump. Slow enough that the ship stays parked for tens of ticks. */ -private static final long PARK_SPEED = 100_000L; +private static final long PARK_SPEED = HYPERSPACE_JUMP_SPEED; // ---- migrated: VSShipTransitCrewE2ETest ---- @@ -218,14 +219,14 @@ public void aSeatedCrewMemberSurvivesAHyperspaceTransitStillRiding() throws Exce + bot().reportRidingEntity(), bot().reportRidingEntity().get("riding").getAsBoolean()); // Depart into hyperspace at the ship anchor (1,64,1 from transit-setup-piloted). - String begin = exec("artest space transit-begin " + originDim + " 1 64 1"); + String begin = exec("artest space transit-begin " + originDim + " 1 64 1 " + HYPERSPACE_JUMP_SPEED); assertTrue("the transit must begin (departure crossing): " + begin, readBool(begin, "began")); // Advance the jump: tick until it arrives (inTransit == 0), capturing the target cell's slot dim. int targetDim = -1; String lastTick = ""; for (int i = 0; i < 80 && targetDim < 0; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { targetDim = readInt(lastTick, "targetDim"); break; @@ -238,7 +239,7 @@ public void aSeatedCrewMemberSurvivesAHyperspaceTransitStillRiding() throws Exce // drive the retries) and observe the CLIENT until it is riding again in the target dim, bounded. boolean crewSurvived = false; for (int i = 0; i < 60 && !crewSurvived; i++) { - exec("artest space transit-tick"); + exec("artest space transit-tick 10"); bot().waitTicks(2); crewSurvived = bot().reportRidingEntity().get("riding").getAsBoolean() && bot().reportWeather().get("dim").getAsInt() == targetDim; @@ -375,7 +376,7 @@ public void aSeatedCrewMemberIsAboardHisShipInHyperspaceWhileItIsStillFlying() t boolean ridingInFlight = false; String lastTick = ""; for (int i = 0; i < 120; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { break; // arrived - everything after this point is the far end, which is another test's } @@ -519,13 +520,13 @@ public void aCrewMemberIsReseatedOnArrivalWithNothingForcingTheShipLoaded() thro assertTrue("the bot must be seated on the ship BEFORE the jump (control): " + bot().reportRidingEntity(), bot().reportRidingEntity().get("riding").getAsBoolean()); - String begin = execEnvelope("artest space transit-begin " + originDim + " 1 64 1"); + String begin = execEnvelope("artest space transit-begin " + originDim + " 1 64 1 " + HYPERSPACE_JUMP_SPEED); assertTrue("the transit must begin (departure crossing): " + begin, readBool(begin, "began")); int targetDim = -1; String lastTick = ""; for (int i = 0; i < 80 && targetDim < 0; i++) { - lastTick = execEnvelope("artest space transit-tick"); + lastTick = execEnvelope("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { targetDim = readInt(lastTick, "targetDim"); break; @@ -539,7 +540,7 @@ public void aCrewMemberIsReseatedOnArrivalWithNothingForcingTheShipLoaded() thro // is nothing in this world to load it. boolean reseated = false; for (int i = 0; i < RESEAT_POLLS && !reseated; i++) { - execEnvelope("artest space transit-tick"); + execEnvelope("artest space transit-tick 10"); bot().waitTicks(2); reseated = bot().reportRidingEntity().get("riding").getAsBoolean() && bot().reportWeather().get("dim").getAsInt() == targetDim; @@ -694,7 +695,7 @@ public void aJumpAnnouncesItselfInChatOnTheHudAndInTheSky() throws Exception { long tunnelInFlight = -1L; String lastTick = ""; for (int i = 0; i < 120; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { break; } @@ -744,7 +745,7 @@ public void aJumpAnnouncesItselfInChatOnTheHudAndInTheSky() throws Exception { // ── ARRIVAL ───────────────────────────────────────────────────────────────────────────── for (int i = 0; i < 60 && readInt(lastTick, "inTransit") != 0; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); bot().waitTicks(2); } assertEquals("the transit must have finished for the arrival message to be owed: " + lastTick, @@ -941,7 +942,7 @@ public void aCrewMemberLivesInHyperspaceUntilHeStepsOffHisShip() throws Exceptio int hyperDim = -1; String lastTick = ""; for (int i = 0; i < 120; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { break; } @@ -1097,7 +1098,7 @@ public void aCrewMemberLivesInHyperspaceUntilHeStepsOffHisShip() throws Exceptio // scenario shares, with a crew record for a player who is no longer alive to be re-seated. // Ending the transit puts the shared world back the way this scenario found it. for (int i = 0; i < 200; i++) { - if (readInt(exec("artest space transit-tick"), "inTransit") == 0) { + if (readInt(exec("artest space transit-tick 10"), "inTransit") == 0) { break; } bot().waitTicks(2); @@ -1158,7 +1159,7 @@ public void aWalkingCrewMemberTravelsWithHisShipThroughHyperspace() throws Excep String captureInFlight = ""; String lastTick = ""; for (int i = 0; i < 120; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { break; // arrived — the far end is another scenario's subject } @@ -1204,7 +1205,7 @@ public void aWalkingCrewMemberTravelsWithHisShipThroughHyperspace() throws Excep // says nothing about the second. int targetDim = -1; for (int i = 0; i < 120 && targetDim < 0; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { targetDim = readInt(lastTick, "targetDim"); break; @@ -1221,7 +1222,7 @@ public void aWalkingCrewMemberTravelsWithHisShipThroughHyperspace() throws Excep // Drive the placement's retries and watch the CLIENT, exactly as the seated siblings do. boolean carriedOn = false; for (int i = 0; i < RESEAT_POLLS && !carriedOn; i++) { - exec("artest space transit-tick"); + exec("artest space transit-tick 10"); bot().waitTicks(2); carriedOn = bot().reportWeather().get("dim").getAsInt() == targetDim && readBool(exec("artest vs deck-capture"), "alreadyTracked"); @@ -1295,7 +1296,7 @@ public void aStandingCrewMemberStillSeesTheHyperspaceCorridor() throws Exception int hyperDim = -1; String lastTick = ""; for (int i = 0; i < 120; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { break; } @@ -1391,7 +1392,7 @@ public void aCrewMemberWhoStoodUpMidFlightArrivesOnHisFeet() throws Exception { int hyperDim = -1; String lastTick = ""; for (int i = 0; i < 120; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { break; } @@ -1422,7 +1423,7 @@ public void aCrewMemberWhoStoodUpMidFlightArrivesOnHisFeet() throws Exception { // ── FINISH THE JUMP ───────────────────────────────────────────────────────────────────── int targetDim = -1; for (int i = 0; i < 120 && targetDim < 0; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (readInt(lastTick, "inTransit") == 0) { targetDim = readInt(lastTick, "targetDim"); break; @@ -1441,7 +1442,7 @@ public void aCrewMemberWhoStoodUpMidFlightArrivesOnHisFeet() throws Exception { // neither the loss nor the window it happened in. boolean carriedOn = false; for (int i = 0; i < RESEAT_POLLS && !carriedOn; i++) { - exec("artest space transit-tick"); + exec("artest space transit-tick 10"); bot().waitTicks(2); JsonObject state = bot().reportState(); com.google.gson.JsonElement health = state.get("health"); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/HyperspaceSurvivesARestartE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/HyperspaceSurvivesARestartE2ETest.java index d537cff06..5261ac41b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/HyperspaceSurvivesARestartE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/HyperspaceSurvivesARestartE2ETest.java @@ -158,7 +158,7 @@ public void aShipParkedInHyperspaceIsStillThereAfterTheServerRestarts() throws E assertTrue("the departure crossing must put the ship into hyperspace: " + begin, readBool(begin, "began")); - String tick = exec("artest space transit-tick"); + String tick = exec("artest space transit-tick 10"); int hyperDimBefore = readInt(tick, "hyperDim"); int inTransit = readInt(tick, "inTransit"); assertTrue("ARRANGEMENT: the jump must still be in flight when the server goes down, or" @@ -213,7 +213,7 @@ public void aShipParkedInHyperspaceIsStillThereAfterTheServerRestarts() throws E String setupAfter = exec("artest space transit-setup-piloted"); assertTrue("the transit probe stack must come up on boot 2: " + setupAfter, readBool(setupAfter, "ok")); - int hyperDimAfter = readInt(exec("artest space transit-tick"), "hyperDim"); + int hyperDimAfter = readInt(exec("artest space transit-tick 10"), "hyperDim"); int parkedAfter = readIntOr(exec("artest vs ship-count-all " + hyperDimAfter), "count", -1); assertEquals("a ship parked in hyperspace must still be parked in hyperspace after a real" diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSJumpCarriesLooseBodiesE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSJumpCarriesLooseBodiesE2ETest.java index 9361d5939..ffb1c0193 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSJumpCarriesLooseBodiesE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSJumpCarriesLooseBodiesE2ETest.java @@ -6,6 +6,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static zmaster587.advancedRocketry.test.AdvancedRocketryTestConstants.HYPERSPACE_JUMP_SPEED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -72,13 +73,13 @@ public void aJumpCarriesTheBodiesLyingOnItsDeck() throws Exception { assertTrue("ARRANGEMENT: the dropped body must be ABOARD by the definition the crossing uses," + " not merely near the ship: " + dropped, dropped.contains("\"aboard\":true")); - String begin = exec("artest space transit-begin " + originDim + " 1 64 1"); + String begin = exec("artest space transit-begin " + originDim + " 1 64 1 " + HYPERSPACE_JUMP_SPEED); assertTrue("the transit must begin: " + begin, begin.contains("\"began\":true")); int targetDim = -1; String lastTick = ""; for (int i = 0; i < 80 && targetDim < 0; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (extractInt(lastTick, "inTransit") == 0) { targetDim = extractInt(lastTick, "targetDim"); break; @@ -91,7 +92,7 @@ public void aJumpCarriesTheBodiesLyingOnItsDeck() throws Exception { String arrived = ""; boolean carried = false; for (int i = 0; i < 60 && !carried; i++) { - exec("artest space transit-tick"); + exec("artest space transit-tick 10"); arrived = exec("artest vs ship-info " + targetDim + " 0 200 0"); if (arrived.contains("\"posX\"")) { double px = extractDouble(arrived, "posX"); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCellSeamE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCellSeamE2ETest.java index a41516799..742b0bbcb 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCellSeamE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipCellSeamE2ETest.java @@ -26,7 +26,7 @@ * *

    The arrangement uses the REAL on-ramp to get a ship legitimately settled in a cell (assemble, * hold a throttle, climb past the ceiling, let the flight computer's own tick call entry), then moves - * it past the face and drives {@code SpaceSubsystem.seam().requestCarry()} — production code, through + * it past the face and drives {@code SpaceSubsystem.cellCrossings().requestCarry()} — production code, through * a probe verb. The crossing itself is the shared one every other crossing uses.

    * *

    What this test does NOT cover, stated rather than implied: the trigger wiring inside diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipTransitE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipTransitE2ETest.java index d6384742f..d665cdd94 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipTransitE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipTransitE2ETest.java @@ -6,6 +6,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static zmaster587.advancedRocketry.test.AdvancedRocketryTestConstants.HYPERSPACE_JUMP_SPEED; import static org.junit.Assert.assertTrue; /** @@ -38,15 +39,19 @@ public void aVsShipTransitsFromOneCellToAnotherThroughHyperspace() throws Except assertTrue("origin ship never assembled/loaded in the pool-slot cell (dim " + originDim + ")", waitForLoadedShip(originDim) >= 1); - // Depart: begin the jump. The ship leaves the origin cell for hyperspace. - String begin = exec("artest space transit-begin " + originDim + " " + ax + " " + ay + " " + az); + // Depart: begin the jump. The ship leaves the origin cell for hyperspace — at a speed that + // makes it a real flight, because a fast enough jump is performed as a single crossing instead + // and this test is about the hyperspace path. (This fixture could not take the other path + // anyway: its bare cube has no flight computer, so it has no durable id to be crossed under.) + String begin = exec("artest space transit-begin " + originDim + " " + ax + " " + ay + " " + az + + " " + HYPERSPACE_JUMP_SPEED); assertTrue("transit did not begin (departure crossing failed): " + begin, begin.contains("\"began\":true")); // Advance the transit until it arrives (arrival retries while the async hyperspace ship assembles). int targetDim = -1; String lastTick = ""; for (int i = 0; i < 80; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (extractInt(lastTick, "inTransit") == 0) { targetDim = extractInt(lastTick, "targetDim"); break; diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipTransitPersistE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipTransitPersistE2ETest.java index 19402bd2c..3c8d2384c 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSShipTransitPersistE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSShipTransitPersistE2ETest.java @@ -6,6 +6,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static zmaster587.advancedRocketry.test.AdvancedRocketryTestConstants.HYPERSPACE_JUMP_SPEED; import static org.junit.Assert.assertTrue; /** @@ -50,7 +51,8 @@ public void aRestoredInFlightJumpRebuildsItsShipByPastingItsSnapshotIntoTheTarge // Depart into hyperspace. We deliberately do NOT tick the transit yet: it stays parked in hyperspace // while we re-cut its snapshot (the save-point cut is of a PARKED ship). - String begin = exec("artest space transit-begin " + originDim + " " + ax + " " + ay + " " + az); + String begin = exec("artest space transit-begin " + originDim + " " + ax + " " + ay + " " + az + + " " + HYPERSPACE_JUMP_SPEED); assertTrue("transit did not begin (departure crossing failed): " + begin, begin.contains("\"began\":true")); // Re-cut the parked ship's block snapshot; retry while the async hyperspace assembly completes @@ -91,7 +93,7 @@ public void aRestoredInFlightJumpRebuildsItsShipByPastingItsSnapshotIntoTheTarge int targetDim = -1; String lastTick = ""; for (int i = 0; i < 80; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (extractInt(lastTick, "inTransit") == 0) { targetDim = extractInt(lastTick, "targetDim"); break; diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSShortJumpCrossesDirectlyE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSShortJumpCrossesDirectlyE2ETest.java new file mode 100644 index 000000000..a586c0c86 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSShortJumpCrossesDirectlyE2ETest.java @@ -0,0 +1,145 @@ +package zmaster587.advancedRocketry.test.server; + +import org.junit.Assume; +import org.junit.Test; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static zmaster587.advancedRocketry.test.AdvancedRocketryTestConstants.DIRECT_JUMP_SPEED; +import static zmaster587.advancedRocketry.test.AdvancedRocketryTestConstants.HYPERSPACE_JUMP_SPEED; + +/** + * E2E: a jump short enough to have no cruise moves a real VS ship between two cells in ONE crossing. + * + *

    The arrival acceptance here is deliberately the SAME body for both mechanisms + * ({@link #arrivesInTheTargetCell}), run once at a speed that selects the direct crossing and once at a + * speed that selects a hyperspace flight. Two mechanisms with two copies of "did it arrive" drift apart + * within weeks, and the copy that stops being maintained is the one whose mechanism nobody is changing + * — which is the one that will break silently.

    + * + *

    What is asserted about the direct path beyond arriving: it never reports a flight in progress. + * That is the whole claim — no lane, no park, no mid-flight for a restart to resume — and it is read + * off the probe's own {@code inTransit}/{@code crossing} pair rather than off how long anything + * took.

    + */ +public class VSShortJumpCrossesDirectlyE2ETest extends AbstractSharedServerTest { + + /** Probe-driven ticks a crossing or a flight gets to complete before the test calls it stuck. */ + private static final int TICK_POLLS = 80; + + @Test + public void aShortJumpArrivesWithoutEverBeingInFlight() throws Exception { + Assume.assumeTrue("needs Valkyrien Skies on the server", serverHasVs()); + exec("artest vs permaload true"); + + String setup = setUpPilotedShip(); + int originDim = extractInt(setup, "originDim"); + + String begin = exec("artest space transit-begin " + originDim + " 1 64 1 " + + DIRECT_JUMP_SPEED); + assertTrue("the short jump must begin: " + begin, begin.contains("\"began\":true")); + assertEquals("a direct crossing is not a flight — nothing may be in transit the moment it " + + "starts, because there is no flight to be in the middle of: " + begin, + 0, extractInt(begin, "inTransit")); + + String lastTick = arrivesInTheTargetCell(); + assertEquals("and nothing was ever in transit while it settled: " + lastTick, + 0, extractInt(lastTick, "inTransit")); + } + + /** + * The control leg, and it is not decoration: it is what makes the assertion above mean "the SPEED + * chose this" rather than "this fixture always does this". Same ship, same cells, same acceptance — + * only the drive is slower, and the jump becomes a flight with a lane under it. + */ + @Test + public void theSameJumpFlownSlowlyStillGoesThroughHyperspace() throws Exception { + Assume.assumeTrue("needs Valkyrien Skies on the server", serverHasVs()); + exec("artest vs permaload true"); + + String setup = setUpPilotedShip(); + int originDim = extractInt(setup, "originDim"); + + String begin = exec("artest space transit-begin " + originDim + " 1 64 1 " + + HYPERSPACE_JUMP_SPEED); + assertTrue("the jump must begin: " + begin, begin.contains("\"began\":true")); + assertEquals("a slow jump IS a flight, and reports one: " + begin, + 1, extractInt(begin, "inTransit")); + + arrivesInTheTargetCell(); + } + + /** + * The shared acceptance: tick until the jump is over, then require the ship to be VS-managed at the + * target cell's pose. Returns the last tick reply so a caller can assert on the mechanism too. + */ + private String arrivesInTheTargetCell() throws Exception { + int targetDim = -1; + String lastTick = ""; + for (int i = 0; i < TICK_POLLS && targetDim < 0; i++) { + lastTick = exec("artest space transit-tick 10"); + if (extractInt(lastTick, "inTransit") == 0 && extractInt(lastTick, "crossing") == 0 + && extractInt(lastTick, "targetDim") >= 0) { + targetDim = extractInt(lastTick, "targetDim"); + break; + } + Thread.sleep(250); + } + assertTrue("the ship never reached the target cell; last tick=" + lastTick, targetDim >= 0); + assertTrue("the ship never (re)loaded in the target cell (dim " + targetDim + "); countAll=" + + exec("artest vs ship-count-all " + targetDim), waitForLoadedShip(targetDim) >= 1); + String dstInfo = exec("artest vs ship-info " + targetDim + " 0 200 0"); + assertTrue("the arrived ship is not VS-managed in the target cell: " + dstInfo, + dstInfo.contains("\"managed\":true")); + return lastTick; + } + + private String setUpPilotedShip() throws Exception { + String setup = exec("artest space transit-setup-piloted"); + assertTrue("piloted transit setup failed: " + setup, setup.contains("\"ok\":true")); + int originDim = extractInt(setup, "originDim"); + assertTrue("the fixture must mint a durable id — a crossing resolves its ship by identity, " + + "never by the anchor every transit fixture shares: " + setup, + setup.contains("\"durableId\":\"") && !setup.contains("\"durableId\":\"\"")); + assertTrue("origin ship never assembled/loaded in the pool-slot cell (dim " + originDim + ")", + waitForLoadedShip(originDim) >= 1); + return setup; + } + + @org.junit.After + public void resetPermaload() throws Exception { + if (serverHasVs()) { + exec("artest vs permaload false"); + } + } + + private String exec(String cmd) throws Exception { + return String.join("\n", client().execute(cmd)); + } + + private boolean serverHasVs() throws Exception { + return exec("artest vs available").contains("\"available\":true"); + } + + private int waitForLoadedShip(int dim) throws Exception { + for (int i = 0; i < 40; i++) { + if (extractInt(exec("artest vs ship-count-all " + dim), "count") >= 1) { + exec("artest vs load-ships " + dim); + int loaded = extractInt(exec("artest vs ship-count " + dim), "count"); + if (loaded >= 1) { + return loaded; + } + } + Thread.sleep(250); + } + return 0; + } + + private static int extractInt(String json, String key) { + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+)").matcher(json); + return m.find() ? Integer.parseInt(m.group(1)) : Integer.MIN_VALUE; + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/VSUnmannedTransitSettlesOnItsPoseE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/VSUnmannedTransitSettlesOnItsPoseE2ETest.java index 0d51ca6d6..0cf451fef 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/VSUnmannedTransitSettlesOnItsPoseE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/VSUnmannedTransitSettlesOnItsPoseE2ETest.java @@ -6,6 +6,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static zmaster587.advancedRocketry.test.AdvancedRocketryTestConstants.HYPERSPACE_JUMP_SPEED; import static org.junit.Assert.assertTrue; /** @@ -50,13 +51,14 @@ public void anUnmannedJumpEndsOnItsPoseNotInThePasteBand() throws Exception { assertTrue("origin ship never registered in the pool-slot cell (dim " + originDim + ")", waitForRegisteredShip(originDim)); - String begin = exec("artest space transit-begin " + originDim + " " + ax + " " + ay + " " + az); + String begin = exec("artest space transit-begin " + originDim + " " + ax + " " + ay + " " + az + + " " + HYPERSPACE_JUMP_SPEED); assertTrue("transit did not begin (departure crossing failed): " + begin, begin.contains("\"began\":true")); String lastTick = ""; for (int i = 0; i < TICK_POLLS; i++) { - lastTick = exec("artest space transit-tick"); + lastTick = exec("artest space transit-tick 10"); if (extractInt(lastTick, "inTransit") == 0 && extractInt(lastTick, "targetDim") >= 0) { break; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ShortJumpCrossesDirectlyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ShortJumpCrossesDirectlyTest.java new file mode 100644 index 000000000..1a4ce8c28 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ShortJumpCrossesDirectlyTest.java @@ -0,0 +1,260 @@ +package zmaster587.advancedRocketry.test.unit; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.junit.Test; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.math.BlockPos; + +import zmaster587.advancedRocketry.space.CellFrames; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.space.HyperspaceTiles; +import zmaster587.advancedRocketry.space.ShipCrossingService; +import zmaster587.advancedRocketry.space.ShipLedger; +import zmaster587.advancedRocketry.space.ShipTransitManager; +import zmaster587.advancedRocketry.space.SlotBinder; +import zmaster587.advancedRocketry.space.SpaceManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * A jump short enough to be over before it presents itself is performed as ONE cell→cell crossing, + * not flown through hyperspace. + * + *

    What these pin is the DECISION and its consequences, counted rather than timed: how many crossings + * a jump costs, whether a lane is taken, whether a snapshot is cut, and what the ledger says while it + * happens. Timing would pin this machine.

    + */ +public class ShortJumpCrossesDirectlyTest { + + private static GalacticCoord cell(long s) { + return GalacticCoord.ofSectorLocal(s, 0L, 0L, 0L, 0L, 0L); + } + + private static final class FakeBinder implements SlotBinder { + final int[] dims; + FakeBinder(int... dims) { this.dims = dims; } + @Override public int[] slotDims() { return dims; } + @Override public void load(int dimId, String cellKey) { } + @Override public void unload(int dimId) { } + @Override public void discard(int dimId) { } + @Override public void deleteStore(String cellKey) { } + } + + /** Counts the hyperspace legs. A direct jump must not touch any of them. */ + private static final class CountingCrosser implements ShipTransitManager.Crosser { + int departs; + int sourceSnapshots; + + @Override + public ShipCrossingService.Crossed departToHyperspace(int srcSlotDim, BlockPos srcAnchor, + String shipId, HyperspaceTiles.Tile tile) { + departs++; + return new ShipCrossingService.Crossed(new BlockPos(0, 200, 0), UUID.randomUUID()); + } + + @Override + public ShipCrossingService.Crossed arriveFromHyperspace(String shipId, HyperspaceTiles.Tile tile, + BlockPos hyperAnchor, int targetSlotDim) { + return new ShipCrossingService.Crossed(new BlockPos(0, 200, 0), UUID.randomUUID()); + } + + @Override + public NBTTagCompound snapshotSource(int srcSlotDim, BlockPos srcAnchor) { + sourceSnapshots++; + return new NBTTagCompound(); + } + } + + /** Counts the direct crossings, and can refuse one. */ + private static final class CountingDirectCrosser implements ShipTransitManager.DirectCrosser { + final List crossings = new ArrayList<>(); + boolean refuse; + + @Override + public boolean crossDirect(String shipId, GalacticCoord origin, int originSlotDim, + BlockPos originAnchor, GalacticCoord target) { + crossings.add(shipId + " " + origin.cellKey() + "->" + target.cellKey()); + return !refuse; + } + } + + private static SpaceManager.Config never() { + return new SpaceManager.Config(SpaceManager.GcPolicy.NEVER, 0L, 0); + } + + /** How far the fixture's two cells are apart, read the way the departure reads it. */ + private static double fixtureDistance() { + return CellFrames.STATIC.distanceBetween(cell(1), cell(2), 0L); + } + + /** Fast enough that the whole leg fits inside the threshold — the direct case. */ + private static long directSpeed() { + return (long) Math.ceil(fixtureDistance() / ShipTransitManager.DIRECT_CROSSING_MAX_TICKS); + } + + /** Slow enough for a real flight: twice the threshold in ticks, so no rounding can reach it. */ + private static long flightSpeed() { + return Math.max(1L, + (long) (fixtureDistance() / (ShipTransitManager.DIRECT_CROSSING_MAX_TICKS * 2.0d))); + } + + /** + * The rule reads a DURATION, and its boundary is the point at which a flight stops having a middle. + * Stated in ticks with no geometry in the way: one block per tick makes distance and duration the + * same number. + */ + @Test + public void theRuleTurnsOverAtTheTickWhereAFlightStopsHavingACruise() { + long n = ShipTransitManager.DIRECT_CROSSING_MAX_TICKS; + + assertTrue("a leg of exactly N ticks has no cruise, so it is a crossing", + ShipTransitManager.isDirectCrossing(n, 1L)); + assertFalse("one tick more and there is a flight to fly", + ShipTransitManager.isDirectCrossing(n + 1, 1L)); + assertTrue("a fast enough drive makes a LONG leg short — the rule keys on duration, never " + + "on how far away the destination is", + ShipTransitManager.isDirectCrossing(n * 1_000_000.0d, 1_000_000L)); + } + + @Test + public void aShortJumpCostsExactlyOneCrossingAndTakesNoLane() { + SpaceManager space = new SpaceManager(new FakeBinder(10, 11), () -> 0L, never()); + HyperspaceTiles tiles = new HyperspaceTiles(); + CountingCrosser hyperspace = new CountingCrosser(); + CountingDirectCrosser direct = new CountingDirectCrosser(); + ShipTransitManager mgr = new ShipTransitManager(space, tiles, hyperspace); + mgr.setDirectCrosser(direct); + + int originDim = space.materialize(cell(1)); + boolean began = mgr.beginTransit("s", cell(1), originDim, new BlockPos(0, 64, 0), + cell(2), directSpeed()); + + assertTrue("the short jump was performed", began); + assertEquals("exactly one crossing", 1, direct.crossings.size()); + assertEquals("and none of them through hyperspace", 0, hyperspace.departs); + assertEquals("no hyperspace lane is taken by a jump that never enters hyperspace", + 0, tiles.inUseCount()); + assertEquals("nothing is in transit: there is no flight to be in the middle of", + 0, mgr.inTransitCount()); + assertFalse(mgr.isInTransit("s")); + } + + @Test + public void aShortJumpCutsNoSnapshotBecauseItHasNoMidFlightToRestore() { + SpaceManager space = new SpaceManager(new FakeBinder(10, 11), () -> 0L, never()); + CountingCrosser hyperspace = new CountingCrosser(); + ShipTransitManager mgr = new ShipTransitManager(space, new HyperspaceTiles(), hyperspace); + mgr.setDirectCrosser(new CountingDirectCrosser()); + + int originDim = space.materialize(cell(1)); + mgr.beginTransit("s", cell(1), originDim, new BlockPos(0, 64, 0), cell(2), directSpeed()); + + assertEquals("the depart-time floor cut exists to survive a restart mid-flight, and a crossing " + + "has no mid-flight; cutting one would persist a record of a jump nothing resumes", + 0, hyperspace.sourceSnapshots); + } + + /** + * The ledger is what a login reads. A row saying IN_TRANSIT resolves the player through the shared + * hyperspace world, so a direct crossing must never wear it — the ship's blocks are in a cell. + */ + @Test + public void aShortJumpNeverEntersTheInTransitState() { + SpaceManager space = new SpaceManager(new FakeBinder(10, 11), () -> 0L, never()); + ShipLedger ledger = new ShipLedger(); + ShipTransitManager mgr = new ShipTransitManager(space, new HyperspaceTiles(), + new CountingCrosser(), ledger, () -> 1000L); + mgr.setDirectCrosser(new CountingDirectCrosser()); + UUID ship = UUID.randomUUID(); + + int originDim = space.materialize(cell(1)); + assertTrue(mgr.beginTransit(ship.toString(), cell(1), originDim, new BlockPos(0, 64, 0), + cell(2), directSpeed())); + + ShipLedger.Entry e = ledger.get(ship); + // The crossing itself settles the row; this manager must not have written IN_TRANSIT over it. + assertTrue("the transit manager must not have put a direct crossing in transit", + e == null || e.state != ShipLedger.State.IN_TRANSIT); + } + + @Test + public void aLongJumpStillFliesThroughHyperspaceWithItsLaneAndItsSnapshot() { + SpaceManager space = new SpaceManager(new FakeBinder(10, 11), () -> 0L, never()); + HyperspaceTiles tiles = new HyperspaceTiles(); + CountingCrosser hyperspace = new CountingCrosser(); + CountingDirectCrosser direct = new CountingDirectCrosser(); + ShipTransitManager mgr = new ShipTransitManager(space, tiles, hyperspace); + mgr.setDirectCrosser(direct); + + int originDim = space.materialize(cell(1)); + boolean began = mgr.beginTransit("s", cell(1), originDim, new BlockPos(0, 64, 0), + cell(2), flightSpeed()); + + assertTrue(began); + assertEquals("a real flight is not a crossing", 0, direct.crossings.size()); + assertEquals("it departs into hyperspace", 1, hyperspace.departs); + assertEquals("holding a lane", 1, tiles.inUseCount()); + assertEquals("and carrying a snapshot, because it HAS a mid-flight to restore", + 1, hyperspace.sourceSnapshots); + assertTrue(mgr.isInTransit("s")); + } + + /** + * A refused crossing is a FAILED jump, not a jump by another route. The pilot has already paid the + * drive's burst against the mechanism he was quoted; quietly flying him through hyperspace instead + * would charge him for one flight and give him another, and would hide the refusal from the log. + */ + @Test + public void aRefusedShortJumpFailsRatherThanFallingBackToHyperspace() { + SpaceManager space = new SpaceManager(new FakeBinder(10, 11), () -> 0L, never()); + HyperspaceTiles tiles = new HyperspaceTiles(); + CountingCrosser hyperspace = new CountingCrosser(); + CountingDirectCrosser direct = new CountingDirectCrosser(); + direct.refuse = true; + ShipTransitManager mgr = new ShipTransitManager(space, tiles, hyperspace); + mgr.setDirectCrosser(direct); + + int originDim = space.materialize(cell(1)); + boolean began = mgr.beginTransit("s", cell(1), originDim, new BlockPos(0, 64, 0), + cell(2), directSpeed()); + + assertFalse("the jump failed", began); + assertEquals("it was attempted once", 1, direct.crossings.size()); + assertEquals("and not retried down the other path", 0, hyperspace.departs); + assertEquals("no lane was consumed by the failure", 0, tiles.inUseCount()); + assertEquals(0, mgr.inTransitCount()); + } + + /** + * The forecast and the flight must not be able to disagree. There is one predicate and both call + * it, so this pins the property that keeps them together rather than re-deriving the rule: the same + * (distance, speed) pair answers the same way however many times it is asked. + */ + @Test + public void theForecastAndTheDepartureCannotDisagreeBecauseThereIsOnlyOneRule() { + double distance = fixtureDistance(); + long speed = directSpeed(); + + boolean quoted = ShipTransitManager.isDirectCrossing(distance, speed); + + SpaceManager space = new SpaceManager(new FakeBinder(10, 11), () -> 0L, never()); + HyperspaceTiles tiles = new HyperspaceTiles(); + CountingDirectCrosser direct = new CountingDirectCrosser(); + ShipTransitManager mgr = new ShipTransitManager(space, tiles, new CountingCrosser()); + mgr.setDirectCrosser(direct); + int originDim = space.materialize(cell(1)); + mgr.beginTransit("s", cell(1), originDim, new BlockPos(0, 64, 0), cell(2), speed); + + boolean executed = !direct.crossings.isEmpty(); + assertEquals("what the console would quote is what the drive performed", quoted, executed); + assertNull("and the lane allocator was never asked for one", + tiles.inUseCount() == 0 ? null : "a lane was taken"); + } +} From cd6a84b150291ac7e2527e190a779fa2c46bc55e Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 17:57:41 +0300 Subject: [PATCH 29/42] feat: the universe takes its real scale - galaxy reference radius 1500 -> 50000 light years - galaxy type radius bands re-derived from real radii - nucleus subdivision back to its ratified k=215 - BlockDelta reports saturation instead of clamping silently - RegionScan refuses an unwalkable region loudly - systemsInRegion walks only the sub-cells it needs --- .../advancedRocketry/space/AbsolutePos.java | 56 ++++-- .../advancedRocketry/space/BlockDelta.java | 63 ++++++- .../tile/multiblock/TileObservatory.java | 38 +++- .../universe/ClusteredGalaxyGenerator.java | 26 ++- .../universe/GalaxyField.java | 7 +- .../universe/GalaxyGenConfig.java | 49 +++-- .../advancedRocketry/universe/RegionScan.java | 45 ++++- .../universe/UniverseScale.java | 42 +++-- .../test/unit/CellFramesTest.java | 57 ++++++ .../test/unit/GalaxyFieldTest.java | 171 ++++++++++++++---- .../unit/InterstellarLegDistanceTest.java | 84 +++++++++ .../test/unit/TelescopeRegionScanTest.java | 104 +++++++++++ 12 files changed, 634 insertions(+), 108 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/space/AbsolutePos.java b/src/main/java/zmaster587/advancedRocketry/space/AbsolutePos.java index 5a3a0c679..8ac7c73f2 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/AbsolutePos.java +++ b/src/main/java/zmaster587/advancedRocketry/space/AbsolutePos.java @@ -109,35 +109,61 @@ public AbsolutePos plus(long dx, long dy, long dz) { * The vector FROM {@code from} TO this position — the observer→body direction when * {@code from} is the observer. * - *

    Saturates instead of wrapping. A separation that does not fit in a {@code long} of blocks is - * one between things in different galaxies, where a block vector is not the useful answer anyway; - * what must never happen is that it comes back as a small number pointing the wrong way.

    + *

    Saturates instead of wrapping, and the delta says that it did. A separation past a + * {@code long} of blocks is one between things in different galaxies — roughly 244 000 light + * years out, which the galaxy lattice reaches routinely — and there a block vector is a + * direction rather than a distance. Two things must never happen: that it comes back as a small + * number pointing the wrong way (which is what wrapping would do), and that a clamped vector is + * indistinguishable from a real one (which is what silent saturation did). For a distance at any + * magnitude use {@link #distanceTo}, which is computed from the sector delta and never clamps.

    */ public BlockDelta minus(AbsolutePos from) { - if (from == null) { - return BlockDelta.of(saturatingBlocks(sectorX, localX), - saturatingBlocks(sectorY, localY), saturatingBlocks(sectorZ, localZ)); - } - return BlockDelta.of( - saturatingBlocks(sectorX - from.sectorX, localX - from.localX), - saturatingBlocks(sectorY - from.sectorY, localY - from.localY), - saturatingBlocks(sectorZ - from.sectorZ, localZ - from.localZ)); + AbsolutePos origin = (from == null) ? ORIGIN : from; + long dSectorX = sectorX - origin.sectorX; + long dSectorY = sectorY - origin.sectorY; + long dSectorZ = sectorZ - origin.sectorZ; + long dLocalX = localX - origin.localX; + long dLocalY = localY - origin.localY; + long dLocalZ = localZ - origin.localZ; + + boolean clamped = boundHit(dSectorX, dLocalX) != 0 + || boundHit(dSectorY, dLocalY) != 0 + || boundHit(dSectorZ, dLocalZ) != 0; + long dx = saturatingBlocks(dSectorX, dLocalX); + long dy = saturatingBlocks(dSectorY, dLocalY); + long dz = saturatingBlocks(dSectorZ, dLocalZ); + return clamped ? BlockDelta.saturated(dx, dy, dz) : BlockDelta.of(dx, dy, dz); } /** {@code sectors * CELL + local}, held at the {@code long} bounds rather than wrapping past them. */ private static long saturatingBlocks(long sectors, long local) { + int hit = boundHit(sectors, local); + if (hit != 0) { + return hit > 0 ? Long.MAX_VALUE : Long.MIN_VALUE; + } + return sectors * GalacticCoord.CELL + local; + } + + /** + * Which {@code long} bound {@code sectors * CELL + local} runs into: {@code +1} past the top, + * {@code -1} past the bottom, {@code 0} when it fits. + * + *

    The ONE place the overflow is decided. The clamped value and the flag that reports it are + * both read off this, so a delta cannot come back held at a bound while claiming to be exact.

    + */ + private static int boundHit(long sectors, long local) { if (sectors > Long.MAX_VALUE / GalacticCoord.CELL) { - return Long.MAX_VALUE; + return 1; } if (sectors < Long.MIN_VALUE / GalacticCoord.CELL) { - return Long.MIN_VALUE; + return -1; } long scaled = sectors * GalacticCoord.CELL; long sum = scaled + local; if (((scaled ^ sum) & (local ^ sum)) < 0L) { - return local > 0L ? Long.MAX_VALUE : Long.MIN_VALUE; + return local > 0L ? 1 : -1; } - return sum; + return 0; } /** diff --git a/src/main/java/zmaster587/advancedRocketry/space/BlockDelta.java b/src/main/java/zmaster587/advancedRocketry/space/BlockDelta.java index a90bf8e7c..a6cdbf553 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/BlockDelta.java +++ b/src/main/java/zmaster587/advancedRocketry/space/BlockDelta.java @@ -8,35 +8,82 @@ * of two positions whose frames were moving. The render channel carries one per body (the * observer→body vector), and its length is the true distance at that moment.

    * + *

    A separation can be larger than this type can hold, and it SAYS SO

    + * + *

    Three block {@code long}s reach about 244 000 light years, which is a quarter of the way to the + * nearest galaxy: the universe NAMES separations this type cannot express, and it always did — + * a cell name is a sector triple, so the addressable range is orders wider than a block count. What + * changed is that such a separation is now reachable in play rather than hypothetical.

    + * + *

    So a delta carries {@link #isSaturated()}. A saturated delta holds each over-range component at + * the {@code long} bound — the direction survives, which is what a renderer and a nav computer + * actually read — and its {@link #length()} is a LOWER BOUND on the true distance. What must + * never happen, and is what the flag exists to prevent, is a consumer measuring a clamped vector and + * reporting the number as a distance: for that, ask the two {@link AbsolutePos} for + * {@link AbsolutePos#distanceTo}, which is computed from the sector delta and does not clamp.

    + * *

    Immutable value type.

    */ public final class BlockDelta { - public static final BlockDelta ZERO = new BlockDelta(0L, 0L, 0L); + public static final BlockDelta ZERO = new BlockDelta(0L, 0L, 0L, false); private final long dx; private final long dy; private final long dz; + private final boolean saturated; - private BlockDelta(long dx, long dy, long dz) { + private BlockDelta(long dx, long dy, long dz, boolean saturated) { this.dx = dx; this.dy = dy; this.dz = dz; + this.saturated = saturated; } + /** An EXACT displacement: these three numbers are the whole separation. */ public static BlockDelta of(long dx, long dy, long dz) { - return (dx == 0L && dy == 0L && dz == 0L) ? ZERO : new BlockDelta(dx, dy, dz); + return (dx == 0L && dy == 0L && dz == 0L) ? ZERO : new BlockDelta(dx, dy, dz, false); + } + + /** + * A displacement that ran into the {@code long} bound on at least one axis: the components are + * held at the bound and the value reports itself {@link #isSaturated()}. + * + *

    Named rather than a flag on {@link #of}, because which of the two a caller is producing is + * something it KNOWS — and a boolean at the call site would let it be got wrong silently, + * which is the whole defect this pair exists to close.

    + */ + public static BlockDelta saturated(long dx, long dy, long dz) { + return new BlockDelta(dx, dy, dz, true); } public long dx() { return dx; } public long dy() { return dy; } public long dz() { return dz; } + /** + * {@code true} when at least one component was held at the {@code long} bound, so the components + * are a direction and {@link #length()} is a lower bound rather than a distance. + */ + public boolean isSaturated() { + return saturated; + } + + /** + * The two displacements added. Saturation is CARRIED: a sum involving a clamped vector is itself + * only a lower bound, and losing the flag here would launder one back into an exact answer. + */ public BlockDelta plus(BlockDelta other) { - return other == null ? this : of(dx + other.dx, dy + other.dy, dz + other.dz); + if (other == null) { + return this; + } + long sx = dx + other.dx; + long sy = dy + other.dy; + long sz = dz + other.dz; + return (saturated || other.saturated) ? saturated(sx, sy, sz) : of(sx, sy, sz); } - /** Length in blocks. */ + /** Length in blocks — a LOWER BOUND when {@link #isSaturated()}. */ public double length() { double x = dx; double y = dy; @@ -58,7 +105,8 @@ public boolean equals(Object o) { return false; } BlockDelta other = (BlockDelta) o; - return dx == other.dx && dy == other.dy && dz == other.dz; + return dx == other.dx && dy == other.dy && dz == other.dz + && saturated == other.saturated; } @Override @@ -66,11 +114,12 @@ public int hashCode() { int result = Long.hashCode(dx); result = 31 * result + Long.hashCode(dy); result = 31 * result + Long.hashCode(dz); + result = 31 * result + (saturated ? 1 : 0); return result; } @Override public String toString() { - return "BlockDelta[" + dx + "," + dy + "," + dz + "]"; + return "BlockDelta[" + dx + "," + dy + "," + dz + (saturated ? ",saturated]" : "]"); } } diff --git a/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java b/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java index e96db800b..4108b5691 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java @@ -60,6 +60,9 @@ public class TileObservatory extends TileMultiPowerConsumer implements IModularInventory, IDataInventory, IGuiCallback { + private static final org.apache.logging.log4j.Logger LOGGER = + org.apache.logging.log4j.LogManager.getLogger("AdvancedRocketry|Observatory"); + private final java.util.Map savedDataBusNbt = new java.util.HashMap<>(); final static int openTime = 100; final static int observationTime = 1000; @@ -802,8 +805,12 @@ public boolean beginRegionScan(int dirX, int dirY, int dirZ, int distanceSteps) } // Re-aiming mid-sweep is allowed and costs only the cell in flight: every cell already // resolved is already written to the crystal, so there is nothing else to lose. - activeScan = RegionScan.directed(origin, dirX, dirY, dirZ, distanceSteps, - world.getTotalWorldTime(), RegionScan.Tuning.fromConfig()); + RegionScan aimed = buildScan(() -> RegionScan.directed(origin, dirX, dirY, dirZ, distanceSteps, + world.getTotalWorldTime(), RegionScan.Tuning.fromConfig())); + if (aimed == null) { + return false; + } + activeScan = aimed; passive = false; lastScanDiscoveries = 0; lastScanObscured = 0; @@ -811,6 +818,25 @@ public boolean beginRegionScan(int dirX, int dirY, int dirZ, int distanceSteps) return true; } + /** + * Build a survey, or refuse to start one — a configuration that describes a region no survey can + * walk is reported and declined, never started half-way. + * + *

    {@code RegionScan} refuses such a region rather than clamping its look count, because a + * clamped count reports the sweep complete with most of the region never visited. Here that + * refusal has to become an operator-visible "the machine did not start" plus a line in the log + * naming the setting, since the alternative is a tile that throws out of a GUI action.

    + */ + private RegionScan buildScan(java.util.function.Supplier build) { + try { + return build.get(); + } catch (IllegalArgumentException refused) { + LOGGER.error("the observatory at " + pos + " cannot start a survey: " + + refused.getMessage() + " Check the telescopeScan* settings."); + return null; + } + } + /** Stop looking. Free — an aim the operator regrets must not have to be waited out. */ public boolean abortRegionScan() { if (world == null || world.isRemote || activeScan == null) { @@ -837,8 +863,12 @@ public boolean beginPassiveSweep() { return false; } int radius = Math.max(0, ARConfiguration.getCurrentConfig().telescopePassiveRadiusCells); - activeScan = RegionScan.local(origin, radius, world.getTotalWorldTime(), - RegionScan.Tuning.fromConfig()); + RegionScan sweep = buildScan(() -> RegionScan.local(origin, radius, + world.getTotalWorldTime(), RegionScan.Tuning.fromConfig())); + if (sweep == null) { + return false; + } + activeScan = sweep; passive = true; lastScanDiscoveries = 0; lastScanObscured = 0; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 7186db821..8fa356370 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -256,9 +256,19 @@ public Map systemsInRegion(long seed, GalacticCoord m for (long supY = Math.floorDiv(loY, s); supY <= Math.floorDiv(hiY, s) && !capped; supY++) { for (long supZ = Math.floorDiv(loZ, s); supZ <= Math.floorDiv(hiZ, s) && !capped; supZ++) { int k = subdivisionAt(seed, supX, supY, supZ); - for (long i = 0; i < k && !capped; i++) { - for (long j = 0; j < k && !capped; j++) { - for (long m = 0; m < k && !capped; m++) { + // Only the sub-cells the query box actually reaches. A system seated in a + // sub-cell is placed INSIDE it, so this is exactly the same answer as walking all + // k³ and filtering — and it is the difference between a bounded query and a + // 10⁷-cell walk, because a galactic nucleus divides one coarse cell that finely. + long iLo = subIndex(offsetInCoarse(loX, supX, s), s, k); + long iHi = subIndex(offsetInCoarse(hiX, supX, s), s, k); + long jLo = subIndex(offsetInCoarse(loY, supY, s), s, k); + long jHi = subIndex(offsetInCoarse(hiY, supY, s), s, k); + long mLo = subIndex(offsetInCoarse(loZ, supZ, s), s, k); + long mHi = subIndex(offsetInCoarse(hiZ, supZ, s), s, k); + for (long i = iLo; i <= iHi && !capped; i++) { + for (long j = jLo; j <= jHi && !capped; j++) { + for (long m = mLo; m <= mHi && !capped; m++) { Optional g = systemForLattice(seed, Lattice.of(supX, supY, supZ, i, j, m, k, s)); if (!g.isPresent()) { @@ -867,6 +877,16 @@ private Lattice latticeAt(long seed, long sectorX, long sectorY, long sectorZ) { subIndex(Math.floorMod(sectorZ, s), s, k), k, s); } + /** + * Where a region bound sits inside coarse super-cell {@code sup}, as an offset clamped into it — + * so a bound lying outside the cell reads as its nearest face rather than as a sub-index off the + * end of the lattice. + */ + private static long offsetInCoarse(long sector, long sup, long coarseEdge) { + long offset = sector - sup * coarseEdge; + return Math.min(coarseEdge - 1L, Math.max(0L, offset)); + } + /** Which sub-cell an offset inside a coarse cell falls in, on one axis. */ private static long subIndex(long offsetInCoarse, long coarseEdge, int k) { long index = Math.floorDiv(offsetInCoarse * (long) k, Math.max(1L, coarseEdge)); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java index d9fcf1c45..22239e959 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java @@ -283,9 +283,10 @@ public LightYearVector positionAt(long seed, GalacticCoord cell, long tick) { /** * Where a VOID cell is at tick {@code tick}: carried by the Hubble flow and nothing else. * - *

    Precision out here is identical to precision inside a galaxy, because the offset is measured - * from the cell's origin in the same light-year vocabulary — which is what choosing a galaxy scale - * whose cell fits one {@code long} of blocks bought.

    + *

    Out here a position is stated in LIGHT YEARS, not in blocks, and that is what makes the + * intergalactic regime expressible at all: a galaxy cube is millions of light years across, which + * is orders past what a block {@code long} holds, and the layer never asks one to hold it. The + * cell NAME carries the magnitude (a sector triple) and this vector carries the rest.

    */ public static LightYearVector comovingPositionAt(GalacticCoord cell, long tick) { return LightYearVector.ofCell(cell).scale(Cosmology.scaleFactorAt(tick)); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index feb283542..5f61a1525 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -257,27 +257,44 @@ private static List defaultStarTypes() { * orders — so a spiral is something a player FINDS rather than the default sky. */ private static List defaultGalaxyTypes() { + // The bands are REAL radii, read off a catalogue and stated in light years so they can be + // checked against one — never a multiple of UniverseScale.REFERENCE_GALAXY_RADIUS_LY. They + // were once about a thirtieth of these; multiplying that table back up by the same factor is + // the mistake to avoid, because it gives dwarf galaxies larger than real spirals. Each band + // is instead the range its own class actually occupies: + // dwarf spheroidal Sculptor ~1 000 ly, Fornax ~2 300 ly + // dwarf irregular SMC ~3 500 ly, LMC ~7 000 ly + // spiral M33 ~15 000 ly, Milky Way 50 000 ly, the largest discs past 60 000 ly + // elliptical M87 ~60 000 ly, the cluster-centre giants far past that + // scaleHeightRatio is a FRACTION of the radius, so it needs no re-derivation and the heights + // it now produces are the real ones: a spiral's 0.02 is 1 000 ly at 50 000 ly of radius, + // which is the disc thickness that makes a galaxy's population come out at 10^11. List l = new ArrayList<>(); - // name profile radius band flatten arms km/s core weight - l.add(new GalaxyType("Dwarf Spheroidal", GalaxyProfile.SPHEROID, 120d, 500d, 0.70d, 0, 20d, 0.90d, 700)); - l.add(new GalaxyType("Dwarf Irregular", GalaxyProfile.DISC, 200d, 900d, 0.30d, 0, 50d, 0.60d, 290)); - l.add(new GalaxyType("Spiral", GalaxyProfile.DISC, 900d, 2200d, 0.02d, 2, 220d, 0.08d, 7)); - l.add(new GalaxyType("Barred Spiral", GalaxyProfile.DISC, 1000d, 2500d, 0.02d, 4, 210d, 0.10d, 2)); - l.add(new GalaxyType("Elliptical", GalaxyProfile.SPHEROID, 1500d, 3500d, 0.60d, 0, 40d, 0.50d, 1)); + // name profile radius band (ly) flatten arms km/s core weight + l.add(new GalaxyType("Dwarf Spheroidal", GalaxyProfile.SPHEROID, 500d, 3_000d, 0.70d, 0, 20d, 0.90d, 700)); + l.add(new GalaxyType("Dwarf Irregular", GalaxyProfile.DISC, 2_000d, 10_000d, 0.30d, 0, 50d, 0.60d, 290)); + l.add(new GalaxyType("Spiral", GalaxyProfile.DISC, 15_000d, 60_000d, 0.02d, 2, 220d, 0.08d, 7)); + l.add(new GalaxyType("Barred Spiral", GalaxyProfile.DISC, 20_000d, 75_000d, 0.02d, 4, 210d, 0.10d, 2)); + l.add(new GalaxyType("Elliptical", GalaxyProfile.SPHEROID, 30_000d, 150_000d, 0.60d, 0, 40d, 0.50d, 1)); return Collections.unmodifiableList(l); } /** - * The stock cluster table. + * The stock cluster table, and every subdivision in it is now the real one. * - *

    The subdivisions are NOT the real-galaxy ones, and the reason is the scale choice one - * level up. A real nucleus runs about 10⁷ times the field density, i.e. {@code k = 215}. That - * number belongs to a galaxy of 10¹¹ stars; ours is compressed in RADIUS while the star separation - * stays real, so it holds of the order of a million — and a 5-light-year nucleus at {@code k = 215} - * would hold ninety times its own galaxy's entire population. {@code k = 25} puts a nucleus at - * about a tenth of its galaxy, which is what a real nuclear bulge is. Star separation is the - * primary quantity here and the galaxy accommodates it; the contrast has to follow that choice - * rather than be imported from the uncompressed world.

    + *

    Two of the three always were. An open cluster's and a globular's contrast is measured + * against the FIELD, and the field's density is {@link UniverseScale#MEAN_STAR_SEPARATION_LY} — + * real, and never compressed. So {@code k = 4} really does put about a thousand stars in a + * ten-light-year open cluster and {@code k = 14} about a million in a globular, which is what + * those objects hold.

    + * + *

    The NUCLEUS was the exception, and it no longer is. Its contrast is the one number in + * this table that is a statement about its whole GALAXY, and the galaxy used to be compressed in + * radius while the star separation stayed real — so it held of the order of a million stars, and a + * real nucleus's {@code k = 215} (about 10⁷ times the field) would have put ninety times the + * galaxy's entire population inside five light years. It was held at {@code k = 25} for that + * reason, and the reason is gone: a galaxy at its real radius holds ~10¹¹ systems, and 10⁷ times + * the field over a few light years is the nuclear star cluster a real one has.

    */ private static List defaultClusterTypes() { List l = new ArrayList<>(); @@ -295,7 +312,7 @@ private static List defaultClusterTypes() { * The cluster every galaxy has at its own centre — the richest one, and no special case: it is a * cluster like the others, drawn at the galaxy's centre instead of on the cluster lattice. */ - public static final ClusterType NUCLEUS = new ClusterType("Nucleus", 25, 4d, 8d, 0.4d, 1); + public static final ClusterType NUCLEUS = new ClusterType("Nucleus", 215, 4d, 8d, 0.4d, 1); /** Edge of the cube that holds at most one cluster, in light years. */ public static final double CLUSTER_SPACING_LY = 300d; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java b/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java index 5c1eda1fa..48f245b01 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java @@ -50,6 +50,7 @@ public final class RegionScan { private final int cellsDone; private final int cellsPerStep; private final int ticksPerStep; + private final int totalCells; private RegionScan(GalacticCoord min, GalacticCoord max, long distanceCells, long strideCells, long startTick, long stepDeadline, int cellsDone, int cellsPerStep, @@ -63,6 +64,34 @@ private RegionScan(GalacticCoord min, GalacticCoord max, long distanceCells, lon this.cellsDone = cellsDone; this.cellsPerStep = Math.max(1, cellsPerStep); this.ticksPerStep = Math.max(0, ticksPerStep); + this.totalCells = countLooks(min, max, this.strideCells); + } + + /** + * How many looks the region between two corners holds at {@code stride} — computed once, + * here, and REFUSED rather than clamped when it will not fit an {@code int}. + * + *

    A survey is walked by an {@code int} cursor, so a region with more looks than an {@code int} + * can index is not a long survey: it is one that would report itself complete at 2·10⁹ looks with + * the rest of the region never visited, and progress would read 100 % while the sky was untouched. + * That was unreachable while a scan's reach was a few hundred cells and becomes reachable the + * moment survey ranges grow with the galaxy, so the bound is stated where the survey is built.

    + * + *

    The product is checked in {@code double} first: the three counts are {@code long}s and their + * product overflows one long before it passes an {@code int}, so multiplying to find out would be + * the same silent wrap in a different place. Fifty-three bits of mantissa is far more than a + * comparison against 231 needs.

    + */ + private static int countLooks(GalacticCoord min, GalacticCoord max, long stride) { + long x = countAlong(min.sectorX(), max.sectorX(), stride); + long y = countAlong(min.sectorY(), max.sectorY(), stride); + long z = countAlong(min.sectorZ(), max.sectorZ(), stride); + if ((double) x * (double) y * (double) z > Integer.MAX_VALUE) { + throw new IllegalArgumentException("a survey of " + x + "x" + y + "x" + z + + " looks cannot be walked: " + min.cellKey() + " .. " + max.cellKey() + + " at a stride of " + stride + " cells. Narrow the region or widen the stride."); + } + return (int) (x * y * z); } /** @@ -199,18 +228,20 @@ public int ticksPerStep() { /** * How many cells this survey LOOKS at — not how many the region contains. The two differ by the * stride: a region a hundred territories wide is a hundred looks, not a hundred million cells. - * Bounded at construction; never unbounded. + * Bounded at construction; never unbounded, and never a clamped count standing in for a real one. */ public int totalCells() { - long cells = countAlong(min.sectorX(), max.sectorX()) - * countAlong(min.sectorY(), max.sectorY()) - * countAlong(min.sectorZ(), max.sectorZ()); - return (int) Math.min(Integer.MAX_VALUE, Math.max(0L, cells)); + return totalCells; + } + + /** How many sampled cells one axis of the region holds, at a given stride. */ + private static long countAlong(long lo, long hi, long stride) { + return Math.max(0L, (hi - lo) / Math.max(1L, stride) + 1L); } - /** How many sampled cells one axis of the region holds, at this survey's stride. */ + /** The same, at this survey's own stride — what the sweep order is built from. */ private long countAlong(long lo, long hi) { - return Math.max(0L, (hi - lo) / strideCells + 1L); + return countAlong(lo, hi, strideCells); } /** {@code true} once every cell of the region has been resolved. */ diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java index 0a9073aa9..bc56bfb65 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java @@ -83,23 +83,33 @@ public final class UniverseScale { // ─── The galaxy lattice ──────────────────────────────────────────────────── // One level up, and the same scheme: a cube that holds at most one galaxy, and a galaxy seated // inside it. What is stated here is a REFERENCE SIZE and a RATIO; the separation follows from - // them, and an individual galaxy's radius is drawn per type around the reference. + // them, and an individual galaxy's radius is drawn per type. // - // The reference is deliberately about a thirtieth of a real giant galaxy, and the separation is - // scaled with it so the RATIO stays real. That is what buys the whole void a single primitive: - // an offset inside one galaxy cube has to fit a long, and at real sizes it would not. + // The reference is a REAL galaxy, and the separation is the real one, so the whole layer is at + // its physical scale. It used to be about a thirtieth of that, to keep a galaxy cube inside one + // long of BLOCKS; nothing stores a position that way — every position in this layer is a sector + // triple plus an in-cell offset — so the compression was paying for a representation nobody + // built. What the sector triple actually gives at this scale is measured in GalaxyFieldTest. /** - * The size a galaxy is quoted against, in light years — a mid-sized spiral, holding of the order - * of a million systems at {@link #MEAN_STAR_SEPARATION_LY}. Every type's radius band is drawn - * around it. + * The size a galaxy is quoted against, in light years — a mid-sized spiral, i.e. the Milky Way, + * holding of the order of 1011 systems at {@link #MEAN_STAR_SEPARATION_LY}. * - *

    It is about a thirtieth of a real giant galaxy, and that is the number the whole layer is - * sized by: a position out in the void is an offset from its galaxy cell's origin, so the cell - * edge has to fit a {@code long} of blocks. At real sizes it would not, and the void would need a - * second, coarser representation of its own.

    + *

    It is not itself a bound on anything: it anchors {@link #MEAN_GALAXY_SEPARATION_LY} and it + * is the size {@code GalaxyGenConfig}'s type table is written against. Those bands are stated as + * ABSOLUTE light years, so they can be checked against a real catalogue rather than read as + * ratios nobody can verify — which means that moving this number does not move them, and they + * must be RE-DERIVED from real radii rather than scaled. Scaling the old table by the same + * factor produced dwarf galaxies larger than real spirals.

    + * + *

    The compressed value it replaces was chosen so that a galaxy CUBE fit inside one + * {@code long} of blocks, because a void position was believed to be a block offset from its + * cell's origin. It is not: it is a sector triple plus an in-cell offset, and the sector space + * carries this scale with six orders of headroom. The one place a whole separation is still + * expressed as a block {@code long} is a {@link zmaster587.advancedRocketry.space.BlockDelta}, + * which is now able to say when it could not hold one.

    */ - public static final double REFERENCE_GALAXY_RADIUS_LY = 1_500d; + public static final double REFERENCE_GALAXY_RADIUS_LY = 50_000d; /** * How far apart galaxies stand, in galaxy DIAMETERS. This is the real number — galaxies in a @@ -126,8 +136,14 @@ public final class UniverseScale { * years out would work on one seed and put that content outside its own galaxy on the next. The * floor is expressed as a constraint on which TYPES such a galaxy may be drawn from, never as a * clamp applied afterwards. + * + *

    It is set at the smallest DISC GIANT a real catalogue holds, which is what makes the + * qualifying set "the spirals and the ellipticals" and excludes both dwarf classes. It is a + * separate number from any type's band on purpose: the two are not the same statement, and the + * day a pack widens the spiral band downwards this floor should keep its meaning rather than + * follow it.

    */ - public static final double MIN_AUTHORED_GALAXY_RADIUS_LY = 900d; + public static final double MIN_AUTHORED_GALAXY_RADIUS_LY = 15_000d; /** * Where the universe ORIGIN sits inside the home galaxy, as a fraction of its radius — and it is diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/CellFramesTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/CellFramesTest.java index 1eb68937b..98319a320 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/CellFramesTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/CellFramesTest.java @@ -8,6 +8,7 @@ import zmaster587.advancedRocketry.space.GalacticCoord; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** @@ -104,4 +105,60 @@ public void twoCellsInOneMovingSystemKeepTheirDistanceIfBothRide() { ship.staticFrameDistanceTo(bodyInSameCell), drifting.distanceBetween(ship, bodyInSameCell, 999L), 1e-6); } + + // ── separations wider than a block long ─────────────────────────────────── + + /** + * The furthest apart two sectors can be while a block delta between them still holds. Past this + * the components are clamped, which is the whole subject of the two tests below. + */ + private static final long BLOCK_REACH_SECTORS = Long.MAX_VALUE / GalacticCoord.CELL; + + @Test + public void anOrdinarySeparationIsExactAndSaysSo() { + // The control. Everything inside a galaxy is here, and a delta that reported itself clamped + // when it was not would make the flag below useless by crying wolf. + BlockDelta delta = CellFrames.STATIC.deltaBetween(cell(0L, 0L), cell(1_000_000L, 0L), 0L); + assertFalse("a separation a million cells wide fits a long of blocks and must not be flagged", + delta.isSaturated()); + assertEquals(1_000_000L * GalacticCoord.CELL, delta.dx()); + } + + @Test + public void aSeparationTooWideForABlockLongCOMESBACKSAYINGSO() { + // Deliberately asked for. Two things in different galaxies are further apart than three block + // longs can hold — the galaxy lattice is millions of light years across — and the clamped + // vector that comes back is a DIRECTION, not a distance. What must never happen is that it is + // indistinguishable from a real one: a consumer measuring it would report a separation of + // exactly Long.MAX_VALUE blocks as though it had measured something. + GalacticCoord here = cell(0L, 0L); + GalacticCoord farAway = cell(2L * BLOCK_REACH_SECTORS, 0L); + + BlockDelta delta = CellFrames.STATIC.deltaBetween(here, farAway, 0L); + assertTrue("a separation past the block range must report itself saturated", + delta.isSaturated()); + assertEquals("and must be held at the bound, never wrapped to a small number pointing back", + Long.MAX_VALUE, delta.dx()); + + // The direction survives, which is what the render and nav channels actually read. + assertTrue("the clamped component must keep the sign of the real separation", delta.dx() > 0L); + + // And the distance is still answerable at that magnitude — through the positions, which are + // sectorised, rather than through the delta, which is not. + double honest = CellFrames.STATIC.distanceBetween(here, farAway, 0L); + assertTrue("the true distance must exceed what the clamped vector can express: " + honest + + " vs " + delta.length(), + honest > delta.length()); + } + + @Test + public void addingToASaturatedDeltaDoesNotLaunderItBackIntoAnExactOne() { + // A sum involving a lower bound is a lower bound. Dropping the flag here would let a clamped + // vector re-enter the system as an exact answer one addition later. + BlockDelta clamped = BlockDelta.saturated(Long.MAX_VALUE, 0L, 0L); + assertTrue(clamped.plus(BlockDelta.of(1L, 2L, 3L)).isSaturated()); + assertTrue(BlockDelta.of(1L, 2L, 3L).plus(clamped).isSaturated()); + assertFalse("two exact deltas still add to an exact one", + BlockDelta.of(1L, 0L, 0L).plus(BlockDelta.of(2L, 0L, 0L)).isSaturated()); + } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java index a170a6eb0..09dbfd5a8 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java @@ -269,37 +269,80 @@ public void theVoidBetweenGalaxiesHoldsNoSystems() { } @Test - public void aGalaxyCellFitsInsideOneLongOfBlocks() { - // This is what the galaxy SIZE was chosen for, and it is a structural claim rather than a - // balance one. Out in the void a position is an offset from its galaxy cell's origin, so that - // offset has to span a whole cell; a Milky-Way-sized galaxy at a realistic separation would - // put the cell past the long range and force the void into a second, coarser representation. - // Choosing this scale buys one primitive instead of two. + public void theGalaxyLatticeFitsTheSECTORSPACE_whichIsWhatNamesAPosition() { + // What actually bounds this layer, and what does NOT. + // + // It does not: a galaxy cube no longer fits one long of BLOCKS, and never had to. A position + // here is a cell NAME — a sector triple — plus an offset inside that cell, so the addressable + // range is the sector space, not a block count. This test used to assert the opposite, and + // that false constraint is what the galaxy scale had been compressed thirty-fold to satisfy. long spacing = GalaxyGenConfig.DEFAULT_GALAXY_SPACING; - long limitCells = Long.MAX_VALUE / GalacticCoord.CELL; - assertTrue("a galaxy cell of " + spacing + " cells overflows a long of blocks", - spacing <= limitCells); - // The bound is not the edge but the DIAGONAL: two points in one void cell can be that far - // apart, and a separation that cannot be expressed is a separation that silently wraps. - assertTrue("a galaxy cell's diagonal overflows a long of blocks — the margin is only " - + String.format("%.2f", limitCells / (double) spacing) + "x on the edge", - Math.sqrt(3d) * spacing <= limitCells); + long blockLimitCells = Long.MAX_VALUE / GalacticCoord.CELL; + assertTrue("a galaxy cube that fits a long of blocks means the scale is still compressed: " + + spacing + " cells vs " + blockLimitCells, + spacing > blockLimitCells); + + // It does: the DIAGONAL of a galaxy cube has to be nameable, because a sector coordinate that + // wraps renames the cell. That is the real ceiling and it is orders away. + double diagonal = Math.sqrt(3d) * spacing; + double headroom = Long.MAX_VALUE / diagonal; + System.out.println(String.format( + "galaxy cube %d cells (%.3e ly), diagonal %.3e cells, sector headroom %.2ex", + spacing, UniverseScale.lightYearsForCells(spacing), diagonal, headroom)); + assertTrue("the galaxy lattice must fit the sector space with room to spare — headroom is only " + + String.format("%.2f", headroom) + "x", headroom >= 1000d); } @Test - public void aGalaxyHoldsAPopulationOfTheRightOrder() { - // Estimated rather than counted: sweeping every super-cell of a galaxy is 10^8 draws. The - // profile is integrated by Monte Carlo over the galaxy's own sphere, which is the same - // function the generator consults, so this measures the shipped shape and not a model of it. - // - // The band is deliberately wide — three orders. What it guards is the ORDER: a galaxy holding - // thousands would make interstellar travel a tour of a village, and one holding billions would - // put the cell past the long range the test above depends on. - GalaxyGenConfig config = cfg(GalaxyGenConfig.DEFAULT_GALAXY_DENSITY); - GalaxyField f = new GalaxyField(config); - Galaxy home = f.home(0xC0FFEEL); + public void theReferenceSizeIsTheSizeTheTypeTableIsWrittenAgainst() { + // The reference anchors the galaxy SEPARATION, and the type bands are absolute light years so + // they can be checked against a catalogue. Nothing mechanical tied the two together, so the + // bands could sit two orders from the reference and nothing would notice — which is exactly + // what happened. This is that tie: the reference has to be a size an ordinary spiral IS. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + GalaxyGenConfig.GalaxyType spiral = typeNamed(config, "Spiral"); + assertTrue("the reference galaxy radius (" + UniverseScale.REFERENCE_GALAXY_RADIUS_LY + + " ly) falls outside the spiral band [" + spiral.minRadiusLy + ", " + + spiral.maxRadiusLy + "] — one of the two was moved without the other", + UniverseScale.REFERENCE_GALAXY_RADIUS_LY >= spiral.minRadiusLy + && UniverseScale.REFERENCE_GALAXY_RADIUS_LY <= spiral.maxRadiusLy); + } + + @Test + public void authoredContentIsAdmittedToTheDISCGIANTSandToNoDwarf() { + // The floor is a constraint on the TYPE DRAW, so what it really states is a SET: the classes a + // galaxy holding authored content may be. A floor that slipped below the dwarf-irregular band + // would let a pack's content be seated in an object a few thousand light years across and + // land outside it on the next seed. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + double floor = UniverseScale.MIN_AUTHORED_GALAXY_RADIUS_LY; + for (GalaxyGenConfig.GalaxyType t : config.galaxyTypes) { + boolean dwarf = t.name.startsWith("Dwarf"); + boolean qualifies = t.minRadiusLy >= floor; + assertEquals(t.name + " qualifies for authored content: expected " + !dwarf, + !dwarf, qualifies); + } + } + + private static GalaxyGenConfig.GalaxyType typeNamed(GalaxyGenConfig config, String name) { + for (GalaxyGenConfig.GalaxyType t : config.galaxyTypes) { + if (name.equals(t.name)) { + return t; + } + } + throw new AssertionError("the stock table has no type named " + name); + } + + /** + * The population a galaxy of this shape holds, at the SHIPPED densities. + * + *

    Estimated rather than counted: sweeping every super-cell of a real-sized galaxy is 10¹¹ + * draws. The profile is integrated by Monte Carlo over the galaxy's own sphere, and it is the same + * function the generator consults, so this measures the shipped shape and not a model of it.

    + */ + private static double estimateSystems(Galaxy galaxy, GalaxyGenConfig config) { double superCellLy = UniverseScale.lightYearsForCells(config.minSpacing); - double sphereLy3 = 4d / 3d * Math.PI * Math.pow(home.radiusLy(), 3); + double sphereLy3 = 4d / 3d * Math.PI * Math.pow(galaxy.radiusLy(), 3); double superCells = sphereLy3 / Math.pow(superCellLy, 3); // A fixed LCG, so the estimate is the same number on every run and a red is a real change. @@ -310,23 +353,71 @@ public void aGalaxyHoldsAPopulationOfTheRightOrder() { double[] p = new double[3]; for (int axis = 0; axis < 3; axis++) { state = state * 6364136223846793005L + 1442695040888963407L; - p[axis] = ((state >>> 11) * 0x1.0p-53 - 0.5d) * 2d * home.radiusLy(); + p[axis] = ((state >>> 11) * 0x1.0p-53 - 0.5d) * 2d * galaxy.radiusLy(); } - sum += home.densityAt(p[0], p[1], p[2]); + sum += galaxy.densityAt(p[0], p[1], p[2]); } - // The samples fill the CUBE around the galaxy; the sphere is pi/6 of it, and densityAt is - // already zero outside the radius, so the cube mean scales straight onto the cube's volume. - double cubeLy3 = Math.pow(2d * home.radiusLy(), 3); + // The samples fill the CUBE around the galaxy; densityAt is already zero outside the radius, + // so the cube mean scales straight onto the cube's volume. + double cubeLy3 = Math.pow(2d * galaxy.radiusLy(), 3); double meanOverSphere = (sum / samples) * cubeLy3 / sphereLy3; - double systems = config.density * superCells * meanOverSphere; - - System.out.println("home galaxy " + home + ": ~" + (long) systems + " systems (" - + (long) superCells + " super-cells in its sphere, mean profile " - + String.format("%.5f", meanOverSphere) + ")"); - assertTrue("a galaxy holding only " + (long) systems + " systems is a village", - systems > 1e4d); - assertTrue("a galaxy holding " + (long) systems + " systems is past the scale this " - + "lattice was sized for", systems < 1e7d); + return config.density * superCells * meanOverSphere; + } + + /** A spiral at exactly the reference radius: the galaxy the whole layer is quoted against. */ + private static Galaxy referenceSpiral() { + return new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, + typeNamed(GalaxyGenConfig.defaults(), "Spiral"), + UniverseScale.REFERENCE_GALAXY_RADIUS_LY, + 0d, 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO); + } + + @Test + public void aReferenceSpiralHoldsTenToTheEleventhSystems() { + // STATED BEFORE THE SWEEP. A galaxy at the reference radius, at the shipped star separation and + // the shipped disc thickness, must come out at the population a real one has: ~10^11. This is + // not a balance pin — it is the arithmetic that made the real scale choosable at all. Size, + // separation and population are ONE fact (pi.R^2.h at h = 1000 ly and ~76 ly^3 per seat), so a + // galaxy that came out at 10^6 here would mean the radius, the separation or the disc height + // had stopped agreeing with each other. + final double EXPECTED_SYSTEMS = 1e11d; + final double TOLERANCE_FACTOR = 3d; + + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + Galaxy reference = referenceSpiral(); + double systems = estimateSystems(reference, config); + + System.out.println(String.format( + "reference spiral r=%.0f ly, disc height %.0f ly, star separation %.2f ly" + + " -> ~%.3e systems (expected %.0e +/- x%.0f)", + reference.radiusLy(), reference.radiusLy() * reference.type().scaleHeightRatio, + UniverseScale.MEAN_STAR_SEPARATION_LY, systems, EXPECTED_SYSTEMS, TOLERANCE_FACTOR)); + + assertTrue("a reference-sized galaxy holding ~" + String.format("%.3e", systems) + + " systems is not the 10^11 the scale was taken for", + systems >= EXPECTED_SYSTEMS / TOLERANCE_FACTOR + && systems <= EXPECTED_SYSTEMS * TOLERANCE_FACTOR); + } + + @Test + public void everySeedsHomeGalaxyIsAPlaceOfTheRightOrder() { + // The home galaxy's radius is DRAWN, so its population is not one number — a spiral at the + // small end of its band and a giant elliptical differ by three orders, which is what a drawn + // radius cubed means. The band here is therefore wide on purpose; what it guards is that no + // seed opens on a village, and that none opens on something the lattice cannot address. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + GalaxyField f = new GalaxyField(config); + for (long seed : new long[] {0xC0FFEEL, 1L, 2L, 3L, 17L, 99L}) { + Galaxy home = f.home(seed); + double systems = estimateSystems(home, config); + System.out.println("seed " + seed + " home " + home + ": ~" + + String.format("%.3e", systems) + " systems"); + assertTrue("seed " + seed + "'s home galaxy holds only " + (long) systems + " systems", + systems > 1e9d); + assertTrue("seed " + seed + "'s home galaxy holds " + String.format("%.3e", systems) + + " systems, past the largest galaxy a catalogue has (~10^14 stars)", + systems < 3e14d); + } } // ─── The intergalactic regime (R3 + R8) ──────────────────────────────────── diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java index 826822f7e..296e09bcc 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java @@ -14,6 +14,8 @@ import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.UniverseScale; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; import static org.junit.Assert.assertTrue; @@ -119,6 +121,88 @@ private static GalacticCoord nearestTo(java.util.Collection cells return best; } + // ── the band between the two lattices ───────────────────────────────────── + + /** + * How much further a galaxy crossing is than one interstellar step, as arithmetic on the two + * constants: a reference galaxy's DIAMETER over the mean star separation. STATED HERE, before the + * sweep below measures it through the real generator. + * + *

    At the shipped numbers this is about ×23 641. The measurement can disagree with the + * arithmetic in one way that matters: if the generator's actual nearest-neighbour distance drifts + * away from the separation it is configured with, the two lattices are not the scales apart the + * design believes they are.

    + */ + private static final double DECLARED_STAR_TO_GALAXY_BAND = + 2d * UniverseScale.REFERENCE_GALAXY_RADIUS_LY / UniverseScale.MEAN_STAR_SEPARATION_LY; + + /** + * How wide a band the drive ladder needs to have a SECOND TIER in it at all — the reason the + * universe was taken to its real scale rather than a design preference. A tier buys an order of + * magnitude or so of speed; a star→galaxy gap narrower than this leaves no rung above the first, + * and with no rung there is nothing for the research branch or the technology unlocks to open. + */ + private static final double MIN_BAND_FOR_A_SECOND_DRIVE_TIER = 1_000d; + + @Test + public void crossingAGalaxyIsWideEnoughAboveOneStepToHoldASecondDriveTier() { + System.out.println(String.format( + "star -> galaxy band: %.0f x (galaxy diameter %.0f ly / star separation %.2f ly)", + DECLARED_STAR_TO_GALAXY_BAND, 2d * UniverseScale.REFERENCE_GALAXY_RADIUS_LY, + UniverseScale.MEAN_STAR_SEPARATION_LY)); + assertTrue("a star -> galaxy band of only x" + (long) DECLARED_STAR_TO_GALAXY_BAND + + " leaves no room for a drive tier above the first", + DECLARED_STAR_TO_GALAXY_BAND >= MIN_BAND_FOR_A_SECOND_DRIVE_TIER); + } + + @Test + public void theMeasuredBandMatchesTheArithmeticItIsDerivedFrom() { + // The same 20 seeds as the leg reading above, and the same real generator. The lattice is + // STRATIFIED rather than Poisson, so a measured neighbour distance runs somewhat wider than + // the configured edge and the measured band comes out somewhat narrower than the declared one. + // A factor of two is the spread that allows; anything past it means the two lattices are no + // longer the scales apart the drive ladder is derived against. + final double TOLERANCE_FACTOR = 2d; + + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(GalaxyGenConfig.defaults()); + List bands = new ArrayList<>(); + for (long seed = 1L; seed <= 20L; seed++) { + Double stepLy = nearestNeighbourLightYears(gen, seed); + if (stepLy == null) { + continue; + } + bands.add(2d * UniverseScale.REFERENCE_GALAXY_RADIUS_LY / stepLy); + } + Collections.sort(bands); + assertTrue("no seed produced a pair of systems to measure a step from", !bands.isEmpty()); + + double median = bands.get(bands.size() / 2); + System.out.println(String.format( + "measured band over %d seeds: min x%.0f, median x%.0f, max x%.0f (declared x%.0f)", + bands.size(), bands.get(0), median, bands.get(bands.size() - 1), + DECLARED_STAR_TO_GALAXY_BAND)); + + assertTrue("the measured band x" + (long) median + " is not the declared x" + + (long) DECLARED_STAR_TO_GALAXY_BAND + " within a factor of " + + TOLERANCE_FACTOR, + median >= DECLARED_STAR_TO_GALAXY_BAND / TOLERANCE_FACTOR + && median <= DECLARED_STAR_TO_GALAXY_BAND * TOLERANCE_FACTOR); + } + + /** The distance from the system nearest the origin to ITS nearest neighbour, in light years. */ + private static Double nearestNeighbourLightYears(ClusteredGalaxyGenerator gen, long seed) { + Map found = gen.systemsInRegion(seed, + cell(-SEARCH_RADIUS_CELLS, -SEARCH_RADIUS_CELLS, -SEARCH_RADIUS_CELLS), + cell(SEARCH_RADIUS_CELLS, SEARCH_RADIUS_CELLS, SEARCH_RADIUS_CELLS)); + GalacticCoord home = nearestTo(found.keySet(), cell(0L, 0L, 0L)); + GalacticCoord neighbour = home == null ? null : nearestTo(found.keySet(), home); + if (neighbour == null) { + return null; + } + double blocks = CellFrames.STATIC.distanceBetween(home, neighbour, 0L); + return blocks / (double) AstronomicalBodyHelper.BLOCKS_PER_LIGHT_YEAR; + } + @Test public void aFartherTargetCostsStrictlyMoreTicksThanANearerOne() { double near = CellFrames.STATIC.distanceBetween(cell(0, 0, 0), cell(4, 0, 0), 0L); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java index 40a35b646..73479d685 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java @@ -9,6 +9,7 @@ import zmaster587.advancedRocketry.navigation.CrystalEntry; import zmaster587.advancedRocketry.navigation.CrystalMemory; import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; import zmaster587.advancedRocketry.universe.EmptyGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.InfoTier; @@ -222,6 +223,27 @@ public void theLocalRadarWalksCellsNotTerritories() { assertTrue("and it must look at the cell the instrument is standing in", looksAtHome); } + @Test + public void aRegionWithMoreLooksThanCanBeWalkedIsREFUSEDratherThanClamped() { + // A survey is walked by an int cursor, and its look count used to be CLAMPED to fit one. A + // clamped count does not make the sweep long — it makes it report itself complete at 2·10⁹ + // looks with the rest of the region never visited, and progress read 100 % while the sky was + // untouched. The local radar is the reachable route: its radius is a config number and a cell + // stride cubes it, so ~1 300 cells of radius is already past an int. + try { + RegionScan.local(HOME, 2_000, 0L, tuning()); + fail("a region of (2*2000+1)^3 looks cannot be walked and must be refused, not clamped"); + } catch (IllegalArgumentException expected) { + assertTrue("the refusal must name what it could not do: " + expected.getMessage(), + expected.getMessage().contains("cannot be walked")); + } + + // And the boundary is not a cliff into silence: one that DOES fit is accepted and counted. + RegionScan fits = RegionScan.local(HOME, 100, 0L, tuning()); + assertEquals("a region that fits must be counted exactly, never rounded", + 201 * 201 * 201, fits.totalCells()); + } + @Test public void aSurveyWithNoDirectionIsRefused() { try { @@ -345,6 +367,88 @@ public void aSystemIsFoundFromAnyCellItOWNS_notOnlyFromItsStarsSeat() { crystal.forBody(401)); } + /** + * The real generator, counting every question the survey asks it. + * + *

    A wrapper rather than a mock, because the claim under test is about the REAL galaxy: the one + * that now holds of the order of 10¹¹ systems. A test against an empty generator would pass by + * having nothing to enumerate.

    + */ + private static final class CountingGenerator implements zmaster587.advancedRocketry.universe.IGalaxyGenerator { + + private final ClusteredGalaxyGenerator real; + int queries; + + CountingGenerator(GalaxyGenConfig config) { + this.real = new ClusteredGalaxyGenerator(config); + } + + @Override + public java.util.Optional systemAt( + long seed, GalacticCoord coord) { + queries++; + return real.systemAt(seed, coord); + } + + @Override + public java.util.Map + systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { + queries++; + return real.systemsInRegion(seed, min, max); + } + + @Override + public java.util.Optional anchorAt(long seed, GalacticCoord cell) { + queries++; + return real.anchorAt(seed, cell); + } + + @Override + public java.util.List bodiesFor(long seed, GalacticCoord systemCoord) { + queries++; + return real.bodiesFor(seed, systemCoord); + } + + @Override + public int minSpacingCells() { + return real.minSpacingCells(); + } + } + + @Test + public void aSurveyResolvesPerLookAndNeverWalksTheGalaxy() { + // The claim the scale change rests on: a galaxy holding 10^11 systems is affordable ONLY + // because nothing ever enumerates one. A survey asks a bounded number of questions — a + // constant per look — and that number is a property of the INSTRUMENT, not of how much sky + // there is. A full-galaxy walk introduced anywhere on this path would blow the bound by nine + // orders, and this test would not merely fail: it would never return. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + CountingGenerator counting = new CountingGenerator(config); + UniverseRegistry.setGenerator(counting); + UniverseRegistry.setStarLookup(TelescopeRegionScanTest::star); + + UniverseRegistry registry = new UniverseRegistry(); + registry.bindWorldSeed(0xC0FFEEL); + + // Aimed at the real reach, through the real config's own stride. + RegionScan.Tuning live = new RegionScan.Tuning(100d, 1, 512, 100, 50d, 4, + config.minSpacing); + RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, live.maxRangeSteps(), 0L, live); + int looks = scan.totalCells(); + assertTrue("the fixture must be a real sweep", looks >= 27); + + CrystalMemory crystal = new CrystalMemory(); + TelescopeScan.resolveBatch(registry, scan, 0, looks, crystal, 7_000L, dimId -> "Body-" + dimId); + + // A handful of questions per look: which system owns the cell, and what that system holds. + int budget = looks * 8; + System.out.println("survey of " + looks + " looks asked the generator " + counting.queries + + " questions (budget " + budget + ")"); + assertTrue("a survey asked the generator " + counting.queries + " questions for " + looks + + " looks — something on this path is enumerating rather than resolving", + counting.queries <= budget); + } + @Test public void aLookIntoTheVoidDiscoversNothing() { // The gate exists so that empty sky does not manufacture addresses — and the fix must not From 329ece8ea6db95aee7e1abe8ebadec7a4d8ba52a Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 18:05:25 +0300 Subject: [PATCH 30/42] fix: a partially specified galaxy type inherits the stock spiral - the galaxyType reader's defaults were a stale copy of it - a pack writing one attribute got a 900 ly "spiral" - defaults now read off GalaxyGenConfig.stockSpiral() - weight stays 1: an unweighted type is the rarest --- .../universe/GalaxyGenConfig.java | 18 +++++++++++++ .../util/XMLPlanetLoader.java | 23 +++++++++------- .../test/integration/XMLPlanetLoaderTest.java | 26 +++++++++++++++++++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index 5f61a1525..cce81f748 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -256,6 +256,24 @@ private static List defaultStarTypes() { * The stock galaxy table. Weights are the real abundance ordering — dwarfs outnumber giants by two * orders — so a spiral is something a player FINDS rather than the default sky. */ + /** + * The stock SPIRAL archetype — the type every partially-specified {@code } inherits + * its unwritten attributes from. + * + *

    It exists so those defaults are not a second copy of the numbers below. They were, and the + * copy went stale the moment the galaxy scale moved: a pack writing + * {@code } got a "spiral" 900–2 200 ly across, an order and a + * half under every real one, silently and only in the authored path.

    + */ + public static GalaxyType stockSpiral() { + for (GalaxyType t : defaultGalaxyTypes()) { + if ("Spiral".equals(t.name)) { + return t; + } + } + throw new IllegalStateException("the stock galaxy table must contain a Spiral"); + } + private static List defaultGalaxyTypes() { // The bands are REAL radii, read off a catalogue and stated in light years so they can be // checked against one — never a multiple of UniverseScale.REFERENCE_GALAXY_RADIUS_LY. They diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index 17825f2ee..07f984249 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -329,12 +329,16 @@ private GalaxyGenConfig readGalaxyGen(Node node) { * Parse one {@code } element into a galaxy archetype. * *
    {@code
    -     * 
          * }
    * - *

    Every attribute defaults to the stock spiral's value, so a pack that wants to change only - * how flat a disc is writes only {@code thickness}.

    + *

    Every SHAPE attribute defaults to the stock spiral's value, so a pack that wants to change + * only how flat a disc is writes only {@code thickness}. Those defaults are READ OFF + * {@link GalaxyGenConfig#stockSpiral()} rather than written here: they were literals once, and the + * copy went stale the moment the galaxy scale moved. {@code weight} is the deliberate exception — + * it defaults to {@code 1}, the rarest, because a type a pack did not weight should not silently + * inherit a spiral's abundance.

    */ private static GalaxyGenConfig.GalaxyType readGalaxyType(Node node) { String profileName = attr(node, ATTR_PROFILE); @@ -348,15 +352,16 @@ private static GalaxyGenConfig.GalaxyType readGalaxyType(Node node) { } } String name = attr(node, ATTR_NAME); + GalaxyGenConfig.GalaxyType stock = GalaxyGenConfig.stockSpiral(); return new GalaxyGenConfig.GalaxyType( (name == null || name.trim().isEmpty()) ? "Galaxy" : name.trim(), profile, - attrDouble(node, ATTR_MINRADIUS, 900d), - attrDouble(node, ATTR_MAXRADIUS, 2200d), - attrDouble(node, ATTR_THICKNESS, 0.02d), - attrInt(node, ATTR_ARMS, 2), - attrDouble(node, ATTR_ROTATIONSPEED, 220d), - attrDouble(node, ATTR_COREFRACTION, 0.08d), + attrDouble(node, ATTR_MINRADIUS, stock.minRadiusLy), + attrDouble(node, ATTR_MAXRADIUS, stock.maxRadiusLy), + attrDouble(node, ATTR_THICKNESS, stock.scaleHeightRatio), + attrInt(node, ATTR_ARMS, stock.armCount), + attrDouble(node, ATTR_ROTATIONSPEED, stock.rotationSpeedKmS), + attrDouble(node, ATTR_COREFRACTION, stock.coreRadiusFraction), attrInt(node, ATTR_WEIGHT, 1)); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java b/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java index 230f8cd7b..b4d4ebf7d 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/integration/XMLPlanetLoaderTest.java @@ -558,6 +558,32 @@ public void galaxyTypeChildrenReplaceTheStockTable() throws Exception { assertEquals(5, t.weight); } + @Test + public void aPartiallySpecifiedGalaxyTypeInheritsTheSTOCKspiralNotAFrozenCopyOfIt() + throws Exception { + // The reason this test exists: the reader's defaults used to be literals — 900 / 2200 ly — and + // they went stale the moment the galaxy scale moved, so a pack that wrote only `thickness` got + // a "spiral" an order and a half under every real one, silently and only in the authored path. + // A pack writing one attribute must get the SHIPPED spiral for the rest. + GalaxyGenConfig.GalaxyType stock = GalaxyGenConfig.stockSpiral(); + DimensionPropertyCoupling c = parse(galaxy( + "\n" + + " \n" + + "\n")); + assertNotNull(c.galaxyGenConfig); + GalaxyGenConfig.GalaxyType t = c.galaxyGenConfig.galaxyTypes.get(0); + + assertEquals("thickness is what the pack asked for", 0.25d, t.scaleHeightRatio, 1e-9); + assertEquals("and the radius band is the SHIPPED spiral's", stock.minRadiusLy, + t.minRadiusLy, 1e-9); + assertEquals(stock.maxRadiusLy, t.maxRadiusLy, 1e-9); + assertEquals(stock.armCount, t.armCount); + assertEquals(stock.rotationSpeedKmS, t.rotationSpeedKmS, 1e-9); + assertEquals(stock.coreRadiusFraction, t.coreRadiusFraction, 1e-9); + assertEquals("weight is the deliberate exception: an unweighted type is the rarest", 1, + t.weight); + } + @Test public void galaxyTypesRoundTripThroughWriteXml() throws IOException { // This file is REWRITTEN on every world save, so a table the writer does not emit is a table a From 32f2a57ffb0952ecfe0442844d62ab6a84750bbb Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 18:32:34 +0300 Subject: [PATCH 31/42] feat: a galaxy keeps a retinue of satellites - satellites are children inside the primary's own cube - count and size drawn from the galaxy type's own band - galaxyContainingSector answers which galaxy a point is in - the seat margin is the whole retinue's reach - a nucleus contrast scales to its own galaxy's population --- .../universe/ClusterField.java | 37 ++- .../universe/ClusteredGalaxyGenerator.java | 22 +- .../advancedRocketry/universe/Galaxy.java | 35 ++- .../universe/GalaxyField.java | 285 ++++++++++++++++-- .../universe/GalaxyGenConfig.java | 32 +- .../universe/StarCluster.java | 15 +- .../universe/UniverseScale.java | 53 ++++ .../util/XMLPlanetLoader.java | 9 +- .../test/unit/GalaxyFieldTest.java | 263 +++++++++++++++- .../test/unit/GalaxyTest.java | 14 +- .../test/unit/NebulaTest.java | 2 +- .../test/unit/StarClusterTest.java | 56 +++- 12 files changed, 764 insertions(+), 59 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java index 003f37c57..70733e3fc 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java @@ -72,18 +72,45 @@ public Optional nucleusOf(long seed, Galaxy galaxy) { if (galaxy == null) { return Optional.empty(); } - double u = CellHash.norm(CellHash.of(seed, galaxy.cellX(), galaxy.cellY(), galaxy.cellZ(), - SALT_NUCLEUS_RADIUS)); + // Keyed on the galaxy's own CENTRE, not on its lattice index: a cube holds a primary and its + // satellites, and they share that index — so keying on it would give every galaxy in a group the + // same nucleus, and a satellite's core would be sized by its primary's draw. + double u = CellHash.norm(CellHash.of(seed, galaxy.centre().sectorX(), + galaxy.centre().sectorY(), galaxy.centre().sectorZ(), SALT_NUCLEUS_RADIUS)); GalaxyGenConfig.ClusterType type = GalaxyGenConfig.NUCLEUS; double radiusLy = type.minRadiusLy + u * (type.maxRadiusLy - type.minRadiusLy); long s = config.minSpacing; - return Optional.of(new StarCluster(type, + return Optional.of(new StarCluster(type, nucleusSubdivisionFor(galaxy), Math.floorDiv(galaxy.centre().sectorX(), s), Math.floorDiv(galaxy.centre().sectorY(), s), Math.floorDiv(galaxy.centre().sectorZ(), s), superCellsForLightYears(radiusLy, config.minSpacing))); } + /** + * How finely a galaxy's NUCLEUS divides the lattice — scaled to the galaxy it is the centre of, + * never taken flat from the table. + * + *

    Every other cluster's contrast is measured against the FIELD, whose density is real and the same + * everywhere; a nucleus's is a statement about its own galaxy's POPULATION, so it cannot be one + * number. The table's {@code k} is the real figure for a reference-sized galaxy (10⁷× the field + * at 10¹¹ stars), and a galaxy's population goes as its radius cubed, so {@code k} goes as the + * radius: {@code k = k_ref · R / R_ref}. That holds the nucleus at a constant FRACTION of + * whatever it is the centre of.

    + * + *

    Measured, and the reason this is derived at all: the flat {@code k = 215} put ~4·10⁷ stars + * inside a 6-light-year core of a 921-light-year dwarf that holds ~10⁷ altogether — a nucleus + * four times its own galaxy. It is the same error the table's {@code k} was once held down to avoid, + * one level lower, and it became reachable the moment satellite galaxies made small galaxies common. + * A dwarf's nucleus comes out at {@code k = 4}, i.e. barely a concentration, which is what a real + * dwarf spheroidal has.

    + */ + private static int nucleusSubdivisionFor(Galaxy galaxy) { + double scaled = GalaxyGenConfig.NUCLEUS.subdivision + * galaxy.radiusLy() / UniverseScale.REFERENCE_GALAXY_RADIUS_LY; + return (int) Math.max(1L, Math.min(GalaxyGenConfig.NUCLEUS.subdivision, Math.round(scaled))); + } + /** * The cluster seated in cluster cell {@code (cx, cy, cz)}, or empty. * @@ -118,7 +145,9 @@ public Optional clusterAtIndex(long seed, Galaxy galaxy, long cx, l long ox = margin + Math.floorMod(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OX), band); long oy = margin + Math.floorMod(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OY), band); long oz = margin + Math.floorMod(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OZ), band); - return Optional.of(new StarCluster(type, cx * spacingSuperCells + ox, + // The type's own k: an open cluster's and a globular's contrast is measured against the FIELD, + // whose density is real and uniform, so it needs no scaling to the galaxy it sits in. + return Optional.of(new StarCluster(type, type.subdivision, cx * spacingSuperCells + ox, cy * spacingSuperCells + oy, cz * spacingSuperCells + oz, radius)); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index 8fa356370..f221da1b4 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -642,7 +642,8 @@ public List nebulaeAround(long seed, GalacticCoord cell, double radiusLy return Collections.emptyList(); } GalacticCoord c = cell.cellCentre(); - Optional galaxy = galaxies.galaxyOwningSector(seed, c.sectorX(), c.sectorY(), + // The galaxy the observer is INSIDE, so a cell in a satellite sees the satellite's clouds. + Optional galaxy = galaxies.galaxyContainingSector(seed, c.sectorX(), c.sectorY(), c.sectorZ()); if (!galaxy.isPresent()) { return Collections.emptyList(); @@ -669,7 +670,7 @@ public double columnDensityBetween(long seed, GalacticCoord from, GalacticCoord return 0d; } GalacticCoord a = from.cellCentre(); - Optional galaxy = galaxies.galaxyOwningSector(seed, a.sectorX(), a.sectorY(), + Optional galaxy = galaxies.galaxyContainingSector(seed, a.sectorX(), a.sectorY(), a.sectorZ()); return galaxy.isPresent() ? nebulae.columnDensityBetween(seed, galaxy.get(), from, to) : 0d; } @@ -844,7 +845,9 @@ long minEdge() { */ private int subdivisionAt(long seed, long supX, long supY, long supZ) { long s = config.minSpacing; - Optional galaxy = galaxies.galaxyOwningSector(seed, supX * s + s / 2L, + // The CONTAINING galaxy: a cluster inside a satellite belongs to the satellite, and its nucleus + // sits at the satellite's own centre. + Optional galaxy = galaxies.galaxyContainingSector(seed, supX * s + s / 2L, supY * s + s / 2L, supZ * s + s / 2L); if (!galaxy.isPresent()) { return 1; @@ -894,14 +897,19 @@ private static long subIndex(long offsetInCoarse, long coarseEdge, int k) { } /** - * How dense the owning galaxy is at this sector triple, in {@code [0, 1]} — zero in the void and - * zero past a galaxy's declared edge. + * How dense the CONTAINING galaxy is at this sector triple, in {@code [0, 1]} — zero in the void and + * zero past every galaxy's declared edge. * *

    The galaxy cell is a coarse reading of the sector, so this is O(1) and needs no stored index: - * every point belongs to exactly one galaxy cell, and that cell either holds a galaxy or is void.

    + * every point belongs to exactly one galaxy cell, and that cell holds a primary galaxy, its + * satellites, or nothing.

    + * + *

    The containing galaxy, not the cube's owner. A cube holds a primary and its retinue, so + * reading the owner's profile at a point inside a satellite would answer zero — and the satellites + * would be named, addressable and completely empty of stars.

    */ private double galaxyProfileAt(long seed, long sectorX, long sectorY, long sectorZ) { - Optional galaxy = galaxies.galaxyOwningSector(seed, sectorX, sectorY, sectorZ); + Optional galaxy = galaxies.galaxyContainingSector(seed, sectorX, sectorY, sectorZ); if (!galaxy.isPresent()) { return 0d; } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java index dd3acebed..8e2b6bea2 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java @@ -56,6 +56,7 @@ public final class Galaxy { private final long cellX; private final long cellY; private final long cellZ; + private final int satelliteIndex; private final GalacticCoord centre; private final LightYearVector seat; private final LightYearVector peculiarVelocity; @@ -79,6 +80,10 @@ public final class Galaxy { /** * @param cellX the galaxy-lattice index this galaxy is seated in + * @param satelliteIndex {@code 0} for the cube's PRIMARY galaxy, {@code 1..n} for a satellite of + * it. A cube holds one primary and its retinue, so the lattice index alone no + * longer identifies a galaxy — this is what distinguishes them, in the name and + * in every draw made per galaxy rather than per cell * @param centre its centre, as a cell name * @param radiusLy its declared radius in light years — drawn inside {@code type}'s band * @param tilt the angle its pole makes with the static +Y axis, in radians @@ -86,14 +91,16 @@ public final class Galaxy { * @param armPitch the arms' pitch angle in radians (ignored when the type has no arms) * @param armPhase where arm zero starts, in radians * @param peculiarVelocity its comoving velocity in light years per tick — its own motion through - * the expanding universe, on top of the expansion + * the expanding universe, on top of the expansion. A satellite carries its + * PRIMARY's, so a group travels together */ - public Galaxy(long cellX, long cellY, long cellZ, GalacticCoord centre, + public Galaxy(long cellX, long cellY, long cellZ, int satelliteIndex, GalacticCoord centre, GalaxyGenConfig.GalaxyType type, double radiusLy, double tilt, double node, double armPitch, double armPhase, LightYearVector peculiarVelocity) { this.cellX = cellX; this.cellY = cellY; this.cellZ = cellZ; + this.satelliteIndex = Math.max(0, satelliteIndex); this.centre = centre; this.seat = LightYearVector.ofCell(centre); this.peculiarVelocity = (peculiarVelocity == null) ? LightYearVector.ZERO : peculiarVelocity; @@ -186,9 +193,29 @@ public double armPhase() { return armPhase; } - /** This galaxy's designation — procedurally-generated galaxy, named for the cell it is seated in. */ + /** + * {@code 0} for the cube's primary galaxy, {@code 1..n} for one of its satellites. A cube holds a + * primary AND its retinue, so this is the second half of a galaxy's identity. + */ + public int satelliteIndex() { + return satelliteIndex; + } + + /** Whether this galaxy is a satellite of the primary seated in the same cube. */ + public boolean isSatellite() { + return satelliteIndex > 0; + } + + /** + * This galaxy's designation — procedurally-generated galaxy, named for the cell it is seated in, + * and for its place in that cube's retinue when it is not the primary. + * + *

    The suffix is not decoration: a satellite is a destination with an address, and two galaxies in + * one cube sharing a name would be two places a player could neither tell apart nor write down.

    + */ public String name() { - return "PGG-" + cellX + "." + cellY + "." + cellZ; + String cell = "PGG-" + cellX + "." + cellY + "." + cellZ; + return isSatellite() ? cell + "-S" + satelliteIndex : cell; } // ─── Membership and profile ──────────────────────────────────────────────── diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java index 22239e959..e549bee5d 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java @@ -1,5 +1,8 @@ package zmaster587.advancedRocketry.universe; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; import zmaster587.advancedRocketry.space.GalacticCoord; @@ -20,9 +23,20 @@ * *

    Every point is in a galaxy CELL; only some are in a GALAXY

    *

    There is no "nowhere". A cell either holds a galaxy or is entirely void, and inside a cell that - * holds one, a point is inside the galaxy iff it is within the declared radius. Those are two - * different questions and they have two different methods here — {@link #galaxyOwning} names the - * cell's galaxy, {@link Galaxy#containsSector} says whether you are in it.

    + * holds one, a point is inside a galaxy iff it is within some declared radius. Those are different + * questions with different methods here — {@link #galaxyOwning} names the cube's PRIMARY, + * {@link Galaxy#containsSector} says whether you are in one named galaxy, and + * {@link #galaxyContainingSector} answers which of the cube's galaxies you are actually in.

    + * + *

    A cube holds a primary AND its retinue

    + *

    The lattice seats one galaxy per cube 25 diameters wide, so on the lattice alone the nearest + * galaxy is always 25 diameters off — which is the distance to the nearest equal GIANT, not to the + * nearest galaxy of any kind. A real giant keeps company at one to three diameters. So a galaxy draws + * {@link #satellitesOf satellites} as CHILDREN inside its own cube, the way a system draws moons inside + * its primary's cell. Nothing about the representation moves: the cube keeps its size, no + * coordinate gains a field, and a satellite is a {@link Galaxy} value drawn from {@code (seed, cell, + * ordinal)} and stored nowhere. What moves is only that a cube's galaxies now have to be told apart — + * hence {@link Galaxy#satelliteIndex()} and the {@code -Sn} suffix in its name.

    * *

    The home galaxy

    *

    Galaxy cell {@code (0,0,0)} is RESERVED: it always holds a galaxy, seated so that the universe @@ -54,6 +68,26 @@ public final class GalaxyField { private static final long SALT_GALAXY_HEADING = 0x10CL; private static final long SALT_GALAXY_ELEVATION = 0x10DL; private static final long SALT_GALAXY_HOME_ANGLE = 0x10EL; + // The retinue's own draws. A satellite's parameters are drawn from its PRIMARY's cell index with + // the satellite's own ordinal folded into the seed, so two satellites of one galaxy cannot + // correlate and no salt has to be allocated per satellite. + private static final long SALT_SATELLITE_COUNT = 0x10FL; + private static final long SALT_SATELLITE_TYPE = 0x110L; + private static final long SALT_SATELLITE_RADIUS = 0x111L; + private static final long SALT_SATELLITE_DISTANCE = 0x112L; + private static final long SALT_SATELLITE_HEADING = 0x113L; + private static final long SALT_SATELLITE_ELEVATION = 0x114L; + private static final long SALT_SATELLITE_TILT = 0x115L; + private static final long SALT_SATELLITE_NODE = 0x116L; + private static final long SALT_SATELLITE_PITCH = 0x117L; + private static final long SALT_SATELLITE_PHASE = 0x118L; + + /** + * What separates one satellite's draws from the next's. Mixed into the SEED through a multiplier of + * its own, exactly as {@code CellHash.ofBody} does for a system's bodies — added to the salt + * instead, the two would merge and satellite {@code i} would be a near-copy of {@code i+1}. + */ + private static final long SATELLITE_ORDINAL_MIX = 0xD1B54A32D192ED03L; /** Arms are drawn in this pitch band, in degrees — the range real spirals occupy. */ private static final double MIN_ARM_PITCH_DEGREES = 10d; @@ -117,8 +151,14 @@ public static long cellLowCorner(long index, long galaxySpacing) { } /** - * The galaxy whose CELL contains this sector triple, or empty when that cell is void. It does not - * ask whether the point is inside the galaxy — see {@link Galaxy#containsSector} for that. + * The PRIMARY galaxy of the cube this sector triple falls in, or empty when that cube is void. + * + *

    Three questions live near each other and only this one is answered here — which galaxy owns + * this cube, the identity a cube is named and declared against. It does not ask whether the + * point is inside that galaxy ({@link Galaxy#containsSector}), and it does not ask which of the + * cube's galaxies the point is in, because a cube holds the primary AND its satellites + * ({@link #galaxyContainingSector}). A caller that wants a PROFILE, a FRAME or a cluster wants that + * third one; a caller naming the neighbourhood wants this.

    */ public Optional galaxyOwningSector(long seed, long sectorX, long sectorY, long sectorZ) { long s = config.galaxySpacing; @@ -126,11 +166,210 @@ public Optional galaxyOwningSector(long seed, long sectorX, long sectorY galaxyIndex(sectorZ, s)); } - /** The galaxy whose cell contains {@code cell}, or empty when that cell is void. */ + /** The primary galaxy of the cube {@code cell} falls in, or empty when that cube is void. */ public Optional galaxyOwning(long seed, GalacticCoord cell) { return galaxyOwningSector(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ()); } + /** + * The galaxy this sector triple is actually INSIDE — the cube's primary, or one of its satellites, + * or empty out in the void between them. + * + *

    This is the question the placement profile, the frame law and the cluster lattice all ask, and + * the reason it is separate from {@link #galaxyOwningSector} is that a cube holds more than one + * galaxy. Asking the owner and then reading ITS profile would put every satellite's interior at + * density zero — the satellites would be named, addressable and empty.

    + * + *

    The answer is always at most one. A satellite is seated at least one full primary + * DIAMETER out and is at most {@link UniverseScale#MAX_SATELLITE_RADIUS_FRACTION} of the primary's + * radius, so no two spheres in a cube can overlap; the single-answer invariant the whole layer rests + * on is a property of that geometry rather than of a tie-break rule here.

    + * + *

    Cost: the primary is tested first, then the retinue is rejected wholesale by one sphere test + * against {@link UniverseScale#retinueReachLy} before any satellite is drawn. The retinue reaches a + * few diameters and the cube is 25 across, so that rejects ~98 % of the cube's volume — which + * matters, because this runs once per super-cell of every placement query.

    + */ + public Optional galaxyContainingSector(long seed, long sectorX, long sectorY, long sectorZ) { + Optional owner = galaxyOwningSector(seed, sectorX, sectorY, sectorZ); + if (!owner.isPresent()) { + return owner; + } + Galaxy primary = owner.get(); + if (primary.containsSector(sectorX, sectorY, sectorZ)) { + return owner; + } + if (!withinRetinueReach(primary, sectorX, sectorY, sectorZ)) { + return Optional.empty(); + } + for (Galaxy satellite : satellitesOf(seed, primary)) { + if (satellite.containsSector(sectorX, sectorY, sectorZ)) { + return Optional.of(satellite); + } + } + return Optional.empty(); + } + + /** The galaxy {@code cell} is inside — primary, satellite, or none. */ + public Optional galaxyContaining(long seed, GalacticCoord cell) { + return galaxyContainingSector(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ()); + } + + /** + * The satellites of {@code primary} — drawn from {@code (seed, its cell, ordinal)}, stored nowhere, + * exactly as the primary itself is. Empty for a type that keeps none, and for a satellite: the + * retinue is one level deep, because a satellite of a satellite is not a thing a real group has and + * would make the containment answer recursive. + */ + public List satellitesOf(long seed, Galaxy primary) { + if (primary == null || primary.isSatellite()) { + return Collections.emptyList(); + } + GalaxyGenConfig.GalaxyType type = primary.type(); + if (type.maxSatellites <= 0) { + return Collections.emptyList(); + } + long gx = primary.cellX(); + long gy = primary.cellY(); + long gz = primary.cellZ(); + int span = type.maxSatellites - type.minSatellites + 1; + int count = type.minSatellites + + (int) Math.floorMod(CellHash.of(seed, gx, gy, gz, SALT_SATELLITE_COUNT), (long) span); + if (count <= 0) { + return Collections.emptyList(); + } + List retinue = new ArrayList<>(count); + for (int i = 1; i <= count; i++) { + Galaxy satellite = satelliteOf(seed, primary, i); + if (satellite != null) { + retinue.add(satellite); + } + } + return Collections.unmodifiableList(retinue); + } + + /** The satellites of the primary seated in the cube {@code cell} falls in. */ + public List satellitesAround(long seed, GalacticCoord cell) { + Optional primary = galaxyOwning(seed, cell); + return primary.isPresent() ? satellitesOf(seed, primary.get()) + : Collections.emptyList(); + } + + /** + * One satellite: a smaller galaxy of its own type, seated a band of primary DIAMETERS out in an + * isotropic direction, with its own orientation and arms. + * + *

    Its TYPE is drawn from the archetypes whose whole radius band fits under + * {@link UniverseScale#MAX_SATELLITE_RADIUS_FRACTION} of the primary's radius, so "smaller than what + * it orbits" is a constraint on the DRAW and never a clamp on its result — the same shape the + * authored-content floor uses. A primary too small for any type to fit under that fraction keeps no + * satellites, which is the honest answer rather than a forced dwarf.

    + * + *

    Its centre does not move relative to its primary, and that is a measurement rather than + * a simplification: a real satellite's orbit runs to 10⁹ years, three orders slower than the disc + * rotation this layer already establishes is invisible inside one save. It carries the primary's + * peculiar velocity, so the group travels together and the home galaxy's retinue stands as still as + * the home galaxy does.

    + */ + private Galaxy satelliteOf(long seed, Galaxy primary, int ordinal) { + long gx = primary.cellX(); + long gy = primary.cellY(); + long gz = primary.cellZ(); + long ownSeed = seed ^ ((long) ordinal * SATELLITE_ORDINAL_MIX); + + GalaxyGenConfig.GalaxyType type = pickSatelliteType(ownSeed, gx, gy, gz, primary.radiusLy()); + if (type == null) { + return null; + } + double radiusFraction = CellHash.norm( + CellHash.of(ownSeed, gx, gy, gz, SALT_SATELLITE_RADIUS)); + double radiusLy = type.minRadiusLy + radiusFraction * (type.maxRadiusLy - type.minRadiusLy); + + double distanceFraction = CellHash.norm( + CellHash.of(ownSeed, gx, gy, gz, SALT_SATELLITE_DISTANCE)); + double diameters = UniverseScale.MIN_SATELLITE_DISTANCE_IN_DIAMETERS + distanceFraction + * (UniverseScale.MAX_SATELLITE_DISTANCE_IN_DIAMETERS + - UniverseScale.MIN_SATELLITE_DISTANCE_IN_DIAMETERS); + double distanceLy = diameters * 2d * primary.radiusLy(); + + // Isotropic: cos(elevation) uniform rather than the elevation itself, or the retinue would pile + // up over the primary's poles. Deliberately NOT in the primary's plane — real companions are + // scattered around a giant rather than laid out in its disc. + double heading = CellHash.norm(CellHash.of(ownSeed, gx, gy, gz, SALT_SATELLITE_HEADING)) + * 2d * Math.PI; + double cosEl = 2d * CellHash.norm(CellHash.of(ownSeed, gx, gy, gz, SALT_SATELLITE_ELEVATION)) + - 1d; + double sinEl = Math.sqrt(Math.max(0d, 1d - cosEl * cosEl)); + double offX = distanceLy * sinEl * Math.cos(heading); + double offY = distanceLy * cosEl; + double offZ = distanceLy * sinEl * Math.sin(heading); + + GalacticCoord centre = GalacticCoord.ofSectorLocal( + primary.centre().sectorX() + UniverseScale.cellsAt(offX), + primary.centre().sectorY() + UniverseScale.cellsAt(offY), + primary.centre().sectorZ() + UniverseScale.cellsAt(offZ), 0L, 0L, 0L); + + double tilt = Math.acos(2d * CellHash.norm( + CellHash.of(ownSeed, gx, gy, gz, SALT_SATELLITE_TILT)) - 1d); + double node = CellHash.norm(CellHash.of(ownSeed, gx, gy, gz, SALT_SATELLITE_NODE)) + * 2d * Math.PI; + double pitch = Math.toRadians(MIN_ARM_PITCH_DEGREES + + CellHash.norm(CellHash.of(ownSeed, gx, gy, gz, SALT_SATELLITE_PITCH)) + * (MAX_ARM_PITCH_DEGREES - MIN_ARM_PITCH_DEGREES)); + double phase = CellHash.norm(CellHash.of(ownSeed, gx, gy, gz, SALT_SATELLITE_PHASE)) + * 2d * Math.PI; + + return new Galaxy(gx, gy, gz, ordinal, centre, type, radiusLy, tilt, node, pitch, phase, + primary.peculiarVelocity()); + } + + /** + * A satellite's archetype: drawn by weight among the types small enough to be one, or {@code null} + * when the table holds none that small. + */ + private GalaxyGenConfig.GalaxyType pickSatelliteType(long seed, long gx, long gy, long gz, + double primaryRadiusLy) { + double ceiling = UniverseScale.MAX_SATELLITE_RADIUS_FRACTION * primaryRadiusLy; + long total = 0L; + for (GalaxyGenConfig.GalaxyType t : config.galaxyTypes) { + if (t.maxRadiusLy <= ceiling) { + total += t.weight; + } + } + if (total <= 0L) { + return null; + } + long r = Math.floorMod(CellHash.of(seed, gx, gy, gz, SALT_SATELLITE_TYPE), total); + GalaxyGenConfig.GalaxyType last = null; + for (GalaxyGenConfig.GalaxyType t : config.galaxyTypes) { + if (t.maxRadiusLy > ceiling) { + continue; + } + last = t; + if (r < t.weight) { + return t; + } + r -= t.weight; + } + return last; + } + + /** + * Whether a point is close enough to {@code primary} for any of its satellites to reach it. One + * sphere test that rejects the whole retinue, so the void inside a cube costs nothing. + */ + private static boolean withinRetinueReach(Galaxy primary, long sectorX, long sectorY, + long sectorZ) { + double reach = UniverseScale.retinueReachLy(primary.radiusLy()); + double dx = UniverseScale.lightYearsForCells( + (double) (sectorX - primary.centre().sectorX())); + double dy = UniverseScale.lightYearsForCells( + (double) (sectorY - primary.centre().sectorY())); + double dz = UniverseScale.lightYearsForCells( + (double) (sectorZ - primary.centre().sectorZ())); + return dx * dx + dy * dy + dz * dz <= reach * reach; + } + /** The home galaxy — the one authored content lives in. Present under every seed, by construction. */ public Galaxy home(long seed) { // Reserved, so the Optional is always full; unwrapping it here is what makes that a statement @@ -208,7 +447,7 @@ public Optional galaxyAtIndex(long seed, long gx, long gy, long gz) { * (MAX_ARM_PITCH_DEGREES - MIN_ARM_PITCH_DEGREES)); double phase = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_PHASE)) * 2d * Math.PI; - return Optional.of(new Galaxy(gx, gy, gz, + return Optional.of(new Galaxy(gx, gy, gz, 0, seatOf(seed, gx, gy, gz, radiusLy, tilt, node, home), type, radiusLy, tilt, node, pitch, phase, peculiarVelocityOf(seed, gx, gy, gz, radiusLy, home))); @@ -246,10 +485,13 @@ private LightYearVector peculiarVelocityOf(long seed, long gx, long gy, long gz, speed * sinEl * Math.sin(heading)); } - /** How far a galaxy of this radius may drift before it would touch its own cell's face. */ + /** + * How far a galaxy of this radius may drift before its RETINUE would touch its own cell's face. The + * whole group travels together, so the budget is the group's reach and not the primary's radius. + */ private double driftBudgetLy(double radiusLy) { double halfCellLy = UniverseScale.lightYearsForCells(config.galaxySpacing / 2d); - return Math.max(0d, halfCellLy - radiusLy); + return Math.max(0d, halfCellLy - UniverseScale.retinueReachLy(radiusLy)); } // ─── The intergalactic regime ────────────────────────────────────────────── @@ -261,10 +503,11 @@ private double driftBudgetLy(double radiusLy) { * tick for a moving craft is the frame-flapping this design exists to prevent.

    */ public GalacticFrame frameAt(long seed, GalacticCoord cell) { - Optional galaxy = galaxyOwning(seed, cell); - boolean bound = galaxy.isPresent() - && galaxy.get().containsSector(cell.sectorX(), cell.sectorY(), cell.sectorZ()); - return bound ? GalacticFrame.GALACTIC : GalacticFrame.COMOVING; + // The galaxy the cell is INSIDE, which may be a satellite: a thing in a satellite rides the + // satellite's disc, not the primary's. Reading the cube's owner instead would leave every point + // in every satellite comoving with the void it is demonstrably not in. + return galaxyContaining(seed, cell).isPresent() + ? GalacticFrame.GALACTIC : GalacticFrame.COMOVING; } /** @@ -272,9 +515,8 @@ public GalacticFrame frameAt(long seed, GalacticCoord cell) { * it. The two laws meet here and nowhere else. */ public LightYearVector positionAt(long seed, GalacticCoord cell, long tick) { - Optional galaxy = galaxyOwning(seed, cell); - if (galaxy.isPresent() - && galaxy.get().containsSector(cell.sectorX(), cell.sectorY(), cell.sectorZ())) { + Optional galaxy = galaxyContaining(seed, cell); + if (galaxy.isPresent()) { return galaxy.get().boundPositionOfCellAt(cell, tick); } return comovingPositionAt(cell, tick); @@ -319,10 +561,16 @@ static double webDensity(long gx, long gy, long gz) { * Where the galaxy sits inside its cube: anywhere that leaves it wholly inside, so it never * straddles a face. * - *

    That containment is what keeps three things true at once — at most one galaxy per cell, + *

    That containment is what keeps three things true at once — at most one PRIMARY per cell, * galaxies that cannot overlap, and an O(1) answer to "which galaxy is this point in" that reads * the containing cell and nothing else.

    * + *

    The margin is the whole RETINUE's reach, not the primary's radius. A satellite is + * seated a few diameters out, so a margin sized to the primary alone would let a galaxy near a face + * keep satellites on the wrong side of it — and a galaxy outside its own lattice cell is one the + * index hands to a neighbour, which is the single-answer invariant broken by a number that was + * correct before satellites existed.

    + * *

    The home galaxy is seated AROUND the origin instead — the origin is where authored content * is, so the galaxy has to contain it. Not ON it: the centre of a galaxy is its nucleus, and that * is the last address a shipped solar system should have. The offset puts the origin at a @@ -340,7 +588,8 @@ private GalacticCoord seatOf(long seed, long gx, long gy, long gz, double radius 0L, 0L, 0L); } long s = config.galaxySpacing; - long margin = Math.min(UniverseScale.cellsForLightYears(radiusLy), Math.max(0L, (s - 1L) / 2L)); + long margin = Math.min(UniverseScale.cellsForLightYears(UniverseScale.retinueReachLy(radiusLy)), + Math.max(0L, (s - 1L) / 2L)); long band = Math.max(1L, s - 2L * margin); // The index came from a real sector, so a cell corner is bounded by that sector and the // products below cannot overflow: each is at most the coordinate it was derived from. diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index cce81f748..8d5237b20 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -105,11 +105,25 @@ public static final class GalaxyType { * (strong shear, and arms that wind up). */ public final double coreRadiusFraction; + /** + * How many SATELLITE galaxies a galaxy of this type keeps, as a band — the same shape as the + * radius band above, and stated as two numbers for the same reason: a single maximum would hide + * the decision of whether a giant may have none at all. + * + *

    Real giants essentially all keep company, so the floor is non-zero for them; a dwarf keeps + * none, and {@code 0..0} is how that is said. It is deliberately a handful and not the dozens a + * real catalogue lists: a satellite is a full galaxy resolved on the placement path, so the + * count is a cost per query, and the ultra-faint dwarfs beyond a handful are not destinations + * anybody would fly to.

    + */ + public final int minSatellites; + public final int maxSatellites; public final int weight; public GalaxyType(String name, GalaxyProfile profile, double minRadiusLy, double maxRadiusLy, double scaleHeightRatio, int armCount, double rotationSpeedKmS, - double coreRadiusFraction, int weight) { + double coreRadiusFraction, int minSatellites, int maxSatellites, + int weight) { this.name = (name == null || name.isEmpty()) ? "GALAXY" : name; this.profile = (profile == null) ? GalaxyProfile.DISC : profile; this.minRadiusLy = Math.max(1d, minRadiusLy); @@ -118,6 +132,8 @@ public GalaxyType(String name, GalaxyProfile profile, double minRadiusLy, double this.armCount = Math.max(0, armCount); this.rotationSpeedKmS = Math.max(0d, rotationSpeedKmS); this.coreRadiusFraction = Math.min(1d, Math.max(0.001d, coreRadiusFraction)); + this.minSatellites = Math.max(0, minSatellites); + this.maxSatellites = Math.max(this.minSatellites, maxSatellites); this.weight = Math.max(1, weight); } } @@ -287,13 +303,15 @@ private static List defaultGalaxyTypes() { // scaleHeightRatio is a FRACTION of the radius, so it needs no re-derivation and the heights // it now produces are the real ones: a spiral's 0.02 is 1 000 ly at 50 000 ly of radius, // which is the disc thickness that makes a galaxy's population come out at 10^11. + // Satellites: a dwarf keeps none — it IS somebody's satellite — and a giant keeps a handful, + // never the dozens a real catalogue lists (see minSatellites for why the count is small). List l = new ArrayList<>(); - // name profile radius band (ly) flatten arms km/s core weight - l.add(new GalaxyType("Dwarf Spheroidal", GalaxyProfile.SPHEROID, 500d, 3_000d, 0.70d, 0, 20d, 0.90d, 700)); - l.add(new GalaxyType("Dwarf Irregular", GalaxyProfile.DISC, 2_000d, 10_000d, 0.30d, 0, 50d, 0.60d, 290)); - l.add(new GalaxyType("Spiral", GalaxyProfile.DISC, 15_000d, 60_000d, 0.02d, 2, 220d, 0.08d, 7)); - l.add(new GalaxyType("Barred Spiral", GalaxyProfile.DISC, 20_000d, 75_000d, 0.02d, 4, 210d, 0.10d, 2)); - l.add(new GalaxyType("Elliptical", GalaxyProfile.SPHEROID, 30_000d, 150_000d, 0.60d, 0, 40d, 0.50d, 1)); + // name profile radius band (ly) flatten arms km/s core sats weight + l.add(new GalaxyType("Dwarf Spheroidal", GalaxyProfile.SPHEROID, 500d, 3_000d, 0.70d, 0, 20d, 0.90d, 0, 0, 700)); + l.add(new GalaxyType("Dwarf Irregular", GalaxyProfile.DISC, 2_000d, 10_000d, 0.30d, 0, 50d, 0.60d, 0, 0, 290)); + l.add(new GalaxyType("Spiral", GalaxyProfile.DISC, 15_000d, 60_000d, 0.02d, 2, 220d, 0.08d, 1, 3, 7)); + l.add(new GalaxyType("Barred Spiral", GalaxyProfile.DISC, 20_000d, 75_000d, 0.02d, 4, 210d, 0.10d, 1, 4, 2)); + l.add(new GalaxyType("Elliptical", GalaxyProfile.SPHEROID, 30_000d, 150_000d, 0.60d, 0, 40d, 0.50d, 2, 5, 1)); return Collections.unmodifiableList(l); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/StarCluster.java b/src/main/java/zmaster587/advancedRocketry/universe/StarCluster.java index fe592ec72..2300f52cf 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/StarCluster.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/StarCluster.java @@ -32,14 +32,23 @@ public final class StarCluster { private final GalaxyGenConfig.ClusterType type; + private final int subdivision; private final long centreSuperX; private final long centreSuperY; private final long centreSuperZ; private final long radiusSuperCells; - public StarCluster(GalaxyGenConfig.ClusterType type, long centreSuperX, long centreSuperY, - long centreSuperZ, long radiusSuperCells) { + /** + * @param subdivision how many parts each coarse super-cell inside this cluster is divided into, + * per axis. Stated rather than read off {@code type}, because a NUCLEUS's + * contrast is a statement about its own GALAXY's population and every other + * cluster's is a statement about the field — the two cannot both be a + * constant in one table + */ + public StarCluster(GalaxyGenConfig.ClusterType type, int subdivision, long centreSuperX, + long centreSuperY, long centreSuperZ, long radiusSuperCells) { this.type = type; + this.subdivision = Math.max(1, subdivision); this.centreSuperX = centreSuperX; this.centreSuperY = centreSuperY; this.centreSuperZ = centreSuperZ; @@ -52,7 +61,7 @@ public GalaxyGenConfig.ClusterType type() { /** How many parts each coarse super-cell inside this cluster is divided into, per axis. */ public int subdivision() { - return type.subdivision; + return subdivision; } /** Its radius, in COARSE super-cells — the unit its boundary is snapped to. */ diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java index bc56bfb65..ba496977c 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java @@ -145,6 +145,59 @@ public final class UniverseScale { */ public static final double MIN_AUTHORED_GALAXY_RADIUS_LY = 15_000d; + // ─── A galaxy's retinue ──────────────────────────────────────────────────── + // The lattice holds at most one galaxy per cube and the cube is 25 diameters across, so on the + // lattice alone the nearest galaxy is always 25 diameters away. That is the distance to the nearest + // equal GIANT — Milky Way to Andromeda — and it was standing in for the distance to the nearest + // galaxy of any kind. Real giants keep company far closer: the Large Magellanic Cloud is 1.6 + // diameters out, the Sagittarius dwarf is inside the halo. + // + // So a galaxy draws SATELLITES as children inside its own cube, exactly as a system draws moons + // inside its primary's cell. Nothing about the representation moves: the cube keeps its size, the + // sector space is untouched, and a satellite is a Galaxy value produced from (seed, cell) like any + // other. + + /** + * How far a satellite is seated from its primary, in the primary's DIAMETERS. The band real + * companions occupy: the LMC sits at about 1.6, and the more distant members of a group run to a + * few. + * + *

    Its floor is what keeps a satellite OUTSIDE its primary — a satellite is at least one whole + * diameter out, so even the largest one clears the primary's edge by a comfortable margin, and two + * galaxies never overlap. That is not cosmetic: overlapping spheres would make "which galaxy is + * this point in" a question with two answers, and the whole layer is built on it having one.

    + */ + public static final double MIN_SATELLITE_DISTANCE_IN_DIAMETERS = 1d; + public static final double MAX_SATELLITE_DISTANCE_IN_DIAMETERS = 3d; + + /** + * How large a satellite may be, as a fraction of its primary's radius. A satellite is drawn from + * the galaxy types whose whole band fits under this, so "smaller than what it orbits" is a property + * of the DRAW rather than a clamp applied to its result — the same shape as the authored-content + * floor above. + * + *

    Measured against the real pair it is named for: the LMC is 0.14 of the Milky Way's radius and + * the SMC 0.07, and M32 is about 0.1 of Andromeda. The bound is loose enough to admit a dwarf + * irregular around a large spiral and tight enough to exclude a second giant.

    + */ + public static final double MAX_SATELLITE_RADIUS_FRACTION = 0.3d; + + /** + * How far a galaxy's whole RETINUE reaches from its centre, in light years — the primary's own + * radius, its farthest satellite seat, and that satellite's own radius. + * + *

    This, and not the primary's radius, is what a galaxy must be seated clear of its cube's faces + * by. A margin sized to the primary alone would let a galaxy seated near a face keep satellites + * OUTSIDE the cube, and a galaxy outside its own lattice cell is one the index attributes to a + * neighbour — the single-answer invariant, broken by a number that was right before satellites + * existed.

    + */ + public static double retinueReachLy(double primaryRadiusLy) { + double radius = Math.max(0d, primaryRadiusLy); + double farthestSeat = MAX_SATELLITE_DISTANCE_IN_DIAMETERS * 2d * radius; + return farthestSeat + MAX_SATELLITE_RADIUS_FRACTION * radius; + } + /** * Where the universe ORIGIN sits inside the home galaxy, as a fraction of its radius — and it is * emphatically not the centre. diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index 07f984249..aa5f1be29 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -74,6 +74,8 @@ public class XMLPlanetLoader { private static final String ATTR_ARMS = "arms"; private static final String ATTR_ROTATIONSPEED = "rotationSpeed"; private static final String ATTR_COREFRACTION = "coreFraction"; + private static final String ATTR_MINSATELLITES = "minSatellites"; + private static final String ATTR_MAXSATELLITES = "maxSatellites"; // A planet TYPE preset: the named region of parameter space a world can land in, plus everything // that follows from being that kind of world. Present -> replaces the whole stock table. private static final String ELEMENT_PLANETTYPE = "planetType"; @@ -330,7 +332,8 @@ private GalaxyGenConfig readGalaxyGen(Node node) { * *
    {@code
          * 
    +     *             thickness="0.02" arms="2" rotationSpeed="220" coreFraction="0.08"
    +     *             minSatellites="1" maxSatellites="3" weight="7"/>
          * }
    * *

    Every SHAPE attribute defaults to the stock spiral's value, so a pack that wants to change @@ -362,6 +365,8 @@ private static GalaxyGenConfig.GalaxyType readGalaxyType(Node node) { attrInt(node, ATTR_ARMS, stock.armCount), attrDouble(node, ATTR_ROTATIONSPEED, stock.rotationSpeedKmS), attrDouble(node, ATTR_COREFRACTION, stock.coreRadiusFraction), + attrInt(node, ATTR_MINSATELLITES, stock.minSatellites), + attrInt(node, ATTR_MAXSATELLITES, stock.maxSatellites), attrInt(node, ATTR_WEIGHT, 1)); } @@ -594,6 +599,8 @@ private static Element writeGalaxyGen(Document doc, GalaxyGenConfig cfg) { gt.setAttribute(ATTR_ARMS, Integer.toString(t.armCount)); gt.setAttribute(ATTR_ROTATIONSPEED, Double.toString(t.rotationSpeedKmS)); gt.setAttribute(ATTR_COREFRACTION, Double.toString(t.coreRadiusFraction)); + gt.setAttribute(ATTR_MINSATELLITES, Integer.toString(t.minSatellites)); + gt.setAttribute(ATTR_MAXSATELLITES, Integer.toString(t.maxSatellites)); gt.setAttribute(ATTR_WEIGHT, Integer.toString(t.weight)); e.appendChild(gt); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java index 09dbfd5a8..2f9514aa3 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java @@ -4,6 +4,8 @@ import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -17,6 +19,7 @@ import zmaster587.advancedRocketry.universe.GalaxyField; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.LightYearVector; +import zmaster587.advancedRocketry.universe.StarSystem; import zmaster587.advancedRocketry.universe.UniverseScale; import static org.junit.Assert.assertEquals; @@ -177,7 +180,11 @@ public void aGalaxyNeverStraddlesItsOwnCellFace() { if (!g.isPresent() || GalaxyField.isHomeCell(gx, gy, gz)) { continue; } - long reach = UniverseScale.cellsForLightYears(g.get().radiusLy()); + // The whole RETINUE's reach, not the primary's radius: satellites are children + // inside this cube, and one seated outside it is a galaxy the index would hand to + // a neighbouring cell. + long reach = UniverseScale.cellsForLightYears( + UniverseScale.retinueReachLy(g.get().radiusLy())); assertInsideCell("x", g.get().centre().sectorX(), gx, s, reach); assertInsideCell("y", g.get().centre().sectorY(), gy, s, reach); assertInsideCell("z", g.get().centre().sectorZ(), gz, s, reach); @@ -259,7 +266,11 @@ public void theVoidBetweenGalaxiesHoldsNoSystems() { // remember to apply. ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(1.0d)); Galaxy home = gen.galaxies().home(77L); - long beyond = UniverseScale.cellsForLightYears(home.radiusLy() * 3d); + // Past the whole RETINUE, not just past the primary: a satellite sits one to three diameters + // out, so probing at three radii would be probing inside a galaxy and this test would be + // asserting that a galaxy is empty. The void starts where the group ends. + long beyond = UniverseScale.cellsForLightYears( + UniverseScale.retinueReachLy(home.radiusLy()) * 1.5d); long spacing = GalaxyGenConfig.DEFAULT_MIN_SPACING; for (long i = 0; i < 40; i++) { GalacticCoord probe = GalacticCoord.ofSectorLocal(beyond + i * spacing, 0L, 0L, 0L, 0L, 0L); @@ -366,7 +377,7 @@ private static double estimateSystems(Galaxy galaxy, GalaxyGenConfig config) { /** A spiral at exactly the reference radius: the galaxy the whole layer is quoted against. */ private static Galaxy referenceSpiral() { - return new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, + return new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, typeNamed(GalaxyGenConfig.defaults(), "Spiral"), UniverseScale.REFERENCE_GALAXY_RADIUS_LY, 0d, 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO); @@ -420,6 +431,252 @@ public void everySeedsHomeGalaxyIsAPlaceOfTheRightOrder() { } } + // ─── The retinue: satellite galaxies ─────────────────────────────────────── + + /** The separation in the primary's DIAMETERS — the unit the satellite band is stated in. */ + private static double diametersApart(Galaxy primary, Galaxy satellite) { + double dx = UniverseScale.lightYearsForCells( + (double) (satellite.centre().sectorX() - primary.centre().sectorX())); + double dy = UniverseScale.lightYearsForCells( + (double) (satellite.centre().sectorY() - primary.centre().sectorY())); + double dz = UniverseScale.lightYearsForCells( + (double) (satellite.centre().sectorZ() - primary.centre().sectorZ())); + return Math.sqrt(dx * dx + dy * dy + dz * dz) / (2d * primary.radiusLy()); + } + + @Test + public void aGiantKeepsARetinueAndADwarfKeepsNone() { + // The whole point of the feature: on the lattice alone the nearest galaxy is always 25 + // diameters away, because a cube holds one. A dwarf keeps none — it IS somebody's satellite. + GalaxyField f = field(1.0d); + int giantsWithRetinue = 0; + int checked = 0; + for (long gx = -6L; gx <= 6L; gx++) { + for (long gy = -3L; gy <= 3L; gy++) { + Optional g = f.galaxyAtIndex(31337L, gx, gy, 0L); + if (!g.isPresent()) { + continue; + } + Galaxy primary = g.get(); + int count = f.satellitesOf(31337L, primary).size(); + checked++; + if (primary.type().maxSatellites == 0) { + assertEquals(primary + " keeps no satellites", 0, count); + } else { + assertTrue(primary + " kept " + count + " satellites, outside its type's band [" + + primary.type().minSatellites + ", " + + primary.type().maxSatellites + "]", + count >= primary.type().minSatellites + && count <= primary.type().maxSatellites); + giantsWithRetinue++; + } + } + } + assertTrue("the sweep must find galaxies", checked > 10); + assertTrue("the sweep must find at least one galaxy that HAS a retinue, or this proves" + + " nothing about satellites at all", giantsWithRetinue > 0); + } + + @Test + public void aRetinueIsAPureFunctionOfSeedAndCell() { + // Same rule as the primary: nothing is stored, so two queries about the same group must never + // disagree — including across two GalaxyField instances, which is what a reload really is. + GalaxyField a = field(1.0d); + GalaxyField b = field(1.0d); + Galaxy primary = a.home(0xBEEFL); + List first = a.satellitesOf(0xBEEFL, primary); + List second = b.satellitesOf(0xBEEFL, b.home(0xBEEFL)); + + assertEquals("the retinue must have the same size on a fresh field", first.size(), + second.size()); + for (int i = 0; i < first.size(); i++) { + assertEquals(first.get(i).toString(), second.get(i).toString()); + } + } + + @Test + public void noTwoGalaxiesInACubeOverlap() { + // The single-answer invariant. Two overlapping spheres would make "which galaxy is this point + // in" a question with two answers, and every frame, profile and cluster read rests on it + // having one. It is geometry rather than a tie-break: a satellite is at least one full + // DIAMETER out and at most a fraction of the primary's radius across. + GalaxyField f = field(1.0d); + int pairs = 0; + for (long seed = 1L; seed <= 40L; seed++) { + Galaxy primary = f.home(seed); + List retinue = f.satellitesOf(seed, primary); + for (int i = 0; i < retinue.size(); i++) { + Galaxy s = retinue.get(i); + assertTrue(s + " is not smaller than its primary " + primary, + s.radiusLy() <= UniverseScale.MAX_SATELLITE_RADIUS_FRACTION + * primary.radiusLy()); + double d = diametersApart(primary, s); + assertTrue(s + " sits " + String.format("%.2f", d) + " diameters out, outside the band", + d >= UniverseScale.MIN_SATELLITE_DISTANCE_IN_DIAMETERS * 0.99d + && d <= UniverseScale.MAX_SATELLITE_DISTANCE_IN_DIAMETERS * 1.01d); + assertTrue(s + " overlaps its primary " + primary, + d * 2d * primary.radiusLy() > primary.radiusLy() + s.radiusLy()); + for (int j = i + 1; j < retinue.size(); j++) { + Galaxy other = retinue.get(j); + double sep = separationLy(s, other); + assertTrue(s + " overlaps " + other + " (" + (long) sep + " ly apart)", + sep > s.radiusLy() + other.radiusLy()); + pairs++; + } + } + } + assertTrue("the sweep must compare at least one PAIR of satellites, or the overlap check" + + " between two of them never executed", pairs > 0); + } + + private static double separationLy(Galaxy a, Galaxy b) { + double dx = UniverseScale.lightYearsForCells( + (double) (a.centre().sectorX() - b.centre().sectorX())); + double dy = UniverseScale.lightYearsForCells( + (double) (a.centre().sectorY() - b.centre().sectorY())); + double dz = UniverseScale.lightYearsForCells( + (double) (a.centre().sectorZ() - b.centre().sectorZ())); + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + @Test + public void aSatelliteIsCloserThanTheNEARESTGIANT() { + // The measurement the feature exists for, stated as the comparison rather than as a number: + // the lattice spacing is the giant-to-giant distance and stays real, and the retinue fills in + // what was missing beneath it. + GalaxyField f = field(1.0d); + double lattice = UniverseScale.GALAXY_SEPARATION_IN_DIAMETERS; + int measured = 0; + double nearest = Double.MAX_VALUE; + for (long seed = 1L; seed <= 40L; seed++) { + Galaxy primary = f.home(seed); + for (Galaxy s : f.satellitesOf(seed, primary)) { + nearest = Math.min(nearest, diametersApart(primary, s)); + measured++; + } + } + assertTrue("no seed produced a satellite to measure", measured > 0); + System.out.println(String.format( + "nearest satellite over 40 seeds: %.2f diameters, against a lattice spacing of %.0f", + nearest, lattice)); + assertTrue("a satellite at " + String.format("%.2f", nearest) + " diameters is no closer than" + + " the lattice already put the nearest giant", nearest < lattice); + } + + @Test + public void aSatelliteIsNamedAPARTfromItsPrimary() { + // A satellite is a destination with an address. Two galaxies in one cube sharing a name would + // be two places a player could neither tell apart nor write down. + GalaxyField f = field(1.0d); + Galaxy primary = f.home(0xC0FFEEL); + List retinue = f.satellitesOf(0xC0FFEEL, primary); + assertTrue("the fixture needs a home galaxy WITH a retinue", !retinue.isEmpty()); + + Set names = new HashSet<>(); + assertTrue(names.add(primary.name())); + assertFalse("a primary must not report itself a satellite", primary.isSatellite()); + for (Galaxy s : retinue) { + assertTrue("two galaxies in one cube share the name " + s.name(), names.add(s.name())); + assertTrue(s + " must report itself a satellite", s.isSatellite()); + assertTrue("a satellite's name must be derived from its primary's: " + s.name(), + s.name().startsWith(primary.name() + "-S")); + } + assertEquals("a satellite keeps no retinue of its own — the group is one level deep", + 0, f.satellitesOf(0xC0FFEEL, retinue.get(0)).size()); + } + + @Test + public void aSatelliteIsAPLACE_withStarsOfItsOwn() { + // THE assumption the retinue was designed around, and the one nobody had checked: that the star + // field can be generated at an offset inside a parent's cube. It can — placement reads the + // profile of the galaxy CONTAINING a point, so a satellite is populated by the same generator + // that populates its primary. Had the profile been read off the cube's OWNER instead, every + // satellite would be named, addressable and completely empty, which is what this catches. + GalaxyGenConfig config = cfg(1.0d); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); + GalaxyField f = gen.galaxies(); + + long seed = 0xC0FFEEL; + Galaxy primary = f.home(seed); + List retinue = f.satellitesOf(seed, primary); + assertTrue("the fixture needs a home galaxy WITH a retinue", !retinue.isEmpty()); + Galaxy satellite = retinue.get(0); + + // Its centre is inside it, and the profile there is the SATELLITE's, not zero. + GalacticCoord core = satellite.centre(); + assertEquals("the cell at a satellite's centre must resolve to the satellite", + satellite.toString(), + f.galaxyContaining(seed, core).get().toString()); + assertTrue("a satellite's own profile at its centre must be positive", + satellite.densityAtSector(core.sectorX(), core.sectorY(), core.sectorZ()) > 0d); + assertEquals("and the cube's PRIMARY must read zero there — that is why the containing galaxy" + + " is the one to ask", 0d, + primary.densityAtSector(core.sectorX(), core.sectorY(), core.sectorZ()), 0d); + + // And the generator actually seats systems in it. + long stride = config.minSpacing; + Map found = gen.systemsInRegion(seed, + GalacticCoord.ofSectorLocal(core.sectorX() - 3L * stride, + core.sectorY() - 3L * stride, core.sectorZ() - 3L * stride, 0L, 0L, 0L), + GalacticCoord.ofSectorLocal(core.sectorX() + 3L * stride, + core.sectorY() + 3L * stride, core.sectorZ() + 3L * stride, 0L, 0L, 0L)); + System.out.println("satellite " + satellite + " holds " + found.size() + + " systems in the 7x7x7 territories around its core"); + assertFalse("a satellite with no systems in it is not a place anybody can go to", + found.isEmpty()); + } + + @Test + public void aCellInsideASatelliteIsBOUNDtoTheSATELLITE() { + // The frame decides both rotation and expansion, so getting this wrong does not make a + // satellite slightly wrong — it makes its interior comove with a void it is not in, while the + // primary it orbits turns. + GalaxyField f = field(1.0d); + long seed = 0xC0FFEEL; + Galaxy primary = f.home(seed); + List retinue = f.satellitesOf(seed, primary); + assertTrue("the fixture needs a home galaxy WITH a retinue", !retinue.isEmpty()); + Galaxy satellite = retinue.get(0); + GalacticCoord core = satellite.centre(); + + assertEquals("a cell inside a satellite is bound, not comoving", GalacticFrame.GALACTIC, + f.frameAt(seed, core)); + assertEquals("and its position is the SATELLITE's bound law", + satellite.boundPositionOfCellAt(core, 5_000L).toString(), + f.positionAt(seed, core, 5_000L).toString()); + + // The control: a point in the same cube but in no galaxy is still comoving. + long past = UniverseScale.cellsForLightYears( + UniverseScale.retinueReachLy(primary.radiusLy()) * 1.5d); + GalacticCoord voidCell = GalacticCoord.ofSectorLocal(primary.centre().sectorX() + past, + primary.centre().sectorY(), primary.centre().sectorZ(), 0L, 0L, 0L); + assertEquals("past the whole group, a cell is comoving again", GalacticFrame.COMOVING, + f.frameAt(seed, voidCell)); + } + + @Test + public void aSatelliteCarriesItsPrimarysMotionSoTheGroupTravelsTogether() { + // A group is bound: if a satellite drew its own peculiar velocity it would drift away from the + // galaxy it orbits over the drift horizon. The home galaxy's retinue must stand as still as + // the home galaxy does, or authored content's neighbours would leave it behind. + GalaxyField f = field(1.0d); + for (long seed : new long[] {1L, 7L, 0xC0FFEEL}) { + Galaxy home = f.home(seed); + for (Galaxy s : f.satellitesOf(seed, home)) { + assertEquals("the home galaxy's satellites must not drift either", 0d, + s.peculiarVelocity().length(), 0d); + } + Optional mover = f.galaxyAtIndex(seed, 3L, 1L, -2L); + if (mover.isPresent()) { + for (Galaxy s : f.satellitesOf(seed, mover.get())) { + assertEquals("a satellite travels with its primary", + mover.get().peculiarVelocity().toString(), + s.peculiarVelocity().toString()); + } + } + } + } + // ─── The intergalactic regime (R3 + R8) ──────────────────────────────────── @Test diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java index 7b0da6255..8c8c06445 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java @@ -29,28 +29,28 @@ public class GalaxyTest { private static GalaxyGenConfig.GalaxyType spiral() { return new GalaxyGenConfig.GalaxyType("Spiral", GalaxyGenConfig.GalaxyProfile.DISC, - 900d, 2200d, 0.02d, 2, 220d, 0.08d, 7); + 900d, 2200d, 0.02d, 2, 220d, 0.08d, 1, 3, 7); } private static GalaxyGenConfig.GalaxyType smoothDisc() { return new GalaxyGenConfig.GalaxyType("Smooth", GalaxyGenConfig.GalaxyProfile.DISC, - 900d, 2200d, 0.02d, 0, 220d, 0.08d, 7); + 900d, 2200d, 0.02d, 0, 220d, 0.08d, 1, 3, 7); } private static GalaxyGenConfig.GalaxyType dwarf() { return new GalaxyGenConfig.GalaxyType("Dwarf", GalaxyGenConfig.GalaxyProfile.SPHEROID, - 120d, 500d, 0.70d, 0, 20d, 0.90d, 700); + 120d, 500d, 0.70d, 0, 20d, 0.90d, 0, 0, 700); } /** A galaxy with its plane on the world's XZ plane, so a test can reason in plain coordinates. */ private static Galaxy flat(GalaxyGenConfig.GalaxyType type) { - return new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, type, RADIUS, 0d, 0d, + return new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, type, RADIUS, 0d, 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO); } /** The same galaxy, seated away from the origin and moving — the subject of the R3 laws. */ private static Galaxy adrift(GalacticCoord seat, LightYearVector velocity) { - return new Galaxy(1L, 0L, 0L, seat, smoothDisc(), RADIUS, 0d, 0d, Math.toRadians(20d), 0d, + return new Galaxy(1L, 0L, 0L, 0, seat, smoothDisc(), RADIUS, 0d, 0d, Math.toRadians(20d), 0d, velocity); } @@ -130,9 +130,9 @@ public void aTypeWithNoArmsIsAxisymmetric() { public void orientationRotatesTheDiscWithoutChangingItsShape() { // Two galaxies alike but for their orientation must be the same object seen from elsewhere: // the density a point sees depends on where it is IN THE GALAXY, never on the world axes. - Galaxy flat = new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, 0d, 0d, + Galaxy flat = new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, 0d, 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO); - Galaxy tilted = new Galaxy(0L, 0L, 0L, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, + Galaxy tilted = new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, Math.toRadians(90d), 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO); // The tilted galaxy's pole is +X, so ITS plane is the world's YZ plane. double r = RADIUS * 0.3d; diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java index 126766454..3510e627b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java @@ -39,7 +39,7 @@ private static ClusteredGalaxyGenerator gen() { } private static StarCluster clusterOfType(GalaxyGenConfig.ClusterType type) { - return new StarCluster(type, 100L, 0L, 0L, 2L); + return new StarCluster(type, type.subdivision, 100L, 0L, 0L, 2L); } private static GalaxyGenConfig.ClusterType typeWithGas(double gas) { diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java index 12f67d981..864efc27d 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java @@ -46,7 +46,7 @@ public void theFineLatticeTilesACoarseCellExactly() { // which is exactly the cost a graded spacing was rejected for. for (int k : new int[] {2, 3, 4, 7, 14, 25, 215}) { for (long coarseEdge : new long[] {1_000L, 40_018_890L, 999_983L}) { - StarCluster c = new StarCluster(type(k), 0L, 0L, 0L, 3L); + StarCluster c = new StarCluster(type(k), k, 0L, 0L, 0L, 3L); long covered = 0L; long previousHigh = 0L; for (long i = 0; i < k; i++) { @@ -69,7 +69,7 @@ public void everyOffsetLandsInExactlyOneSubCell() { // be the one whose bounds contain it. A mismatch here is a system addressed by a cell it does // not sit in. long coarseEdge = 40_018_890L; - StarCluster c = new StarCluster(type(25), 0L, 0L, 0L, 3L); + StarCluster c = new StarCluster(type(25), 25, 0L, 0L, 0L, 3L); for (long offset : new long[] {0L, 1L, coarseEdge / 3L, coarseEdge / 2L, coarseEdge - 1L}) { long i = c.subCellIndex(offset, coarseEdge); assertTrue("index " + i + " out of range for offset " + offset, i >= 0 && i < 25); @@ -84,7 +84,7 @@ public void membershipIsAPropertyOfTheCoarseCell() { // Snapped to coarse cell faces, which is what makes the fine lattice tile and what keeps // "which lattice does this coordinate live on" an O(1) question with one answer. The shape // stays a ball, because the test is on the super-cell INDEX rather than on a box. - StarCluster c = new StarCluster(type(4), 10L, 10L, 10L, 3L); + StarCluster c = new StarCluster(type(4), 4, 10L, 10L, 10L, 3L); assertTrue(c.containsSuperCell(10L, 10L, 10L)); assertTrue(c.containsSuperCell(13L, 10L, 10L)); assertFalse(c.containsSuperCell(14L, 10L, 10L)); @@ -102,7 +102,6 @@ public void everyGalaxyHasANucleusAtItsOwnCentre() { Galaxy home = gen.galaxies().home(SEED); Optional nucleus = clusters.nucleusOf(SEED, home); assertTrue(nucleus.isPresent()); - assertEquals(GalaxyGenConfig.NUCLEUS.subdivision, nucleus.get().subdivision()); long s = cfg().minSpacing; assertTrue("the nucleus must cover the galaxy's own centre", @@ -113,6 +112,55 @@ public void everyGalaxyHasANucleusAtItsOwnCentre() { nucleus.get().subdivision() > cfg().clusterTypes.get(0).subdivision); } + @Test + public void aNUCLEUSscalesToItsOwnGalaxyWhileTheOtherClustersDoNot() { + // The distinction the table cannot hold in one number. An open cluster's and a globular's + // contrast is measured against the FIELD, whose density is real and the same everywhere — so it + // is a constant. A NUCLEUS's contrast is a statement about its own galaxy's POPULATION, and the + // table's figure is the real one for a REFERENCE-sized galaxy. + // + // Measured failure it replaces: at a flat k = 215 a 921-light-year dwarf — the size satellite + // galaxies routinely are — got ~4·10^7 stars inside a six-light-year core while holding ~10^7 + // altogether. A nucleus four times its own galaxy. Population goes as radius cubed and k cubed, + // so k has to go as the radius. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg()); + ClusterField clusters = gen.clusters(); + + int atReference = clusters.nucleusOf(SEED, galaxyOfRadius( + UniverseScale.REFERENCE_GALAXY_RADIUS_LY)).get().subdivision(); + assertEquals("a reference-sized galaxy must get the table's own figure", + GalaxyGenConfig.NUCLEUS.subdivision, atReference); + + int atTenth = clusters.nucleusOf(SEED, galaxyOfRadius( + UniverseScale.REFERENCE_GALAXY_RADIUS_LY / 10d)).get().subdivision(); + assertTrue("a galaxy a tenth the size must get a proportionally thinner core, not the same one:" + + " " + atTenth + " vs " + atReference, + atTenth < atReference); + assertTrue("and never below one — a nucleus that refines nothing is still a place", + atTenth >= 1); + + // The bound that matters, said in the units of the defect: a nucleus may not hold more stars + // than the galaxy it is the centre of. Both counts are k^3 x volume against the same field, so + // the comparison needs no population model — just the two volumes. + double dwarfRadius = 921d; + Galaxy dwarf = galaxyOfRadius(dwarfRadius); + int k = clusters.nucleusOf(SEED, dwarf).get().subdivision(); + double coreLy = GalaxyGenConfig.NUCLEUS.maxRadiusLy; + double coreShare = Math.pow((double) k, 3d) * Math.pow(coreLy / dwarfRadius, 3d); + System.out.println(String.format( + "a %.0f ly galaxy gets nucleus k=%d; its core holds %.4f of the galaxy's own stars", + dwarfRadius, k, coreShare)); + assertTrue("a dwarf's nucleus holds " + String.format("%.2f", coreShare) + + " of its whole galaxy", coreShare < 0.5d); + } + + /** A galaxy of a stated radius, at the origin — the subject when the SIZE is what is under test. */ + private static Galaxy galaxyOfRadius(double radiusLy) { + return new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, cfg().galaxyTypes.get(0), radiusLy, + 0d, 0d, Math.toRadians(20d), 0d, + zmaster587.advancedRocketry.universe.LightYearVector.ZERO); + } + @Test public void aClusterNeverStraddlesItsOwnLatticeCell() { // The same containment the galaxy tier needs, one level down and for the same reason: a From 5c8a11b3e822d3078737312092adfe4ef92e735b Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 19:29:39 +0300 Subject: [PATCH 32/42] feat: a drive generation buys efficiency, its size buys power - DriveTier with the galactic band gap derived, not written - JumpSpeed takes the tier; no overload defaults it - routeEnergy: the closed form where power cancels - powerForCoils is the single home of the power law - a dampener absorbs a fraction of a baseline arrival --- .../hyperdrive/DriveTier.java | 81 +++++ .../hyperdrive/DriveTuning.java | 75 ++++- .../hyperdrive/JumpSpeed.java | 61 +++- .../hyperdrive/JumpTrigger.java | 3 +- .../hyperdrive/ShipDriveStats.java | 32 +- .../navigation/ShipNavigation.java | 3 +- .../tile/hyperdrive/TileGravityDampener.java | 9 +- .../hyperdrive/TileHyperdriveGenerator.java | 17 +- .../test/unit/DriveLadderTest.java | 298 ++++++++++++++++++ .../test/unit/HyperdriveStatsTest.java | 21 +- .../unit/InterstellarLegDistanceTest.java | 4 +- 11 files changed, 563 insertions(+), 41 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTier.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTier.java b/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTier.java new file mode 100644 index 000000000..9fbfbd172 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTier.java @@ -0,0 +1,81 @@ +package zmaster587.advancedRocketry.hyperdrive; + +import zmaster587.advancedRocketry.universe.UniverseScale; + +/** + * A hyperdrive's generation, and the only thing it changes: how efficiently power becomes speed. + * + *

    A tier is a coefficient, never a licence

    + *

    There is no permission gate anywhere on this enum. A first-generation drive aimed across + * interstellar space is not refused — it simply goes much slower, which makes the trip unreasonable + * rather than impossible, and the barrier a player then meets is life support and generation without + * sunlight over that duration. Real systems and real risks, not a red message.

    + * + *

    Why the tiers are the bands, and why there are exactly two

    + *

    Distance in this universe is not smooth: it comes in bands separated by orders of magnitude — + * across a system, out to the nearest stars, across a galaxy, out to the next one. Growing a drive + * (more coils) is spent ONCE and closes the first of those gaps; after that the coils are gone, so a + * tier has to pay a WHOLE band gap rather than a residue. A tier therefore exists for each gap that + * building bigger cannot cover, and each one is NAMED for the band it owns.

    + * + *

    The gap out to the next galaxy is only {@link UniverseScale#GALAXY_SEPARATION_IN_DIAMETERS}, far + * below what one generation of drive is worth, so there is no third tier: reaching another galaxy is + * patience at full {@link #GALACTIC}, which is an honest answer rather than a refusal.

    + */ +public enum DriveTier { + + /** + * The drive a player builds himself. Its band is his own neighbourhood of stars, and it closes + * that band by SIZE — the coil count — rather than by efficiency, which is why its efficiency is + * the unit: every other tier is quoted against it. + */ + INTERSTELLAR(1d), + + /** + * The drive that makes a galaxy crossable. Its efficiency is not a chosen number: it IS the gap + * between the two bands, a galaxy's diameter measured in interstellar steps, so it is derived from + * the two lengths the universe layer already declares and moves with them if they ever move. + * + *

    That derivation is the point. Written as a literal it would be a number nobody could check + * and one that silently stopped meaning "one band" the first time the star separation or the + * galaxy size was retuned.

    + */ + GALACTIC(2d * UniverseScale.REFERENCE_GALAXY_RADIUS_LY / UniverseScale.MEAN_STAR_SEPARATION_LY); + + private final double efficiency; + + DriveTier(double efficiency) { + this.efficiency = Math.max(1d, efficiency); + } + + /** + * How much more speed this generation gets out of the same power as {@link #INTERSTELLAR}, which + * is 1 by definition. + * + *

    It sits in the DENOMINATOR of a route's total energy — ticks are {@code d·m/(η·P)} and the + * in-flight draw is proportional to {@code P}, so power cancels and the bill for a leg depends on + * distance, mass and the tier alone. "A tier buys efficiency" is therefore literal arithmetic and + * not a figure of speech.

    + */ + public double efficiency() { + return efficiency; + } + + /** The band this generation is built to cross in the time one band is meant to take. */ + public double bandLightYears() { + return this == GALACTIC + ? 2d * UniverseScale.REFERENCE_GALAXY_RADIUS_LY + : UniverseScale.MEAN_STAR_SEPARATION_LY; + } + + /** The generation every hull has until a later one is built. */ + public static DriveTier baseline() { + return INTERSTELLAR; + } + + /** The tier stored under {@code ordinal}, or the baseline when the value is not one of ours. */ + public static DriveTier byOrdinal(int ordinal) { + DriveTier[] all = values(); + return (ordinal < 0 || ordinal >= all.length) ? baseline() : all[ordinal]; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTuning.java b/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTuning.java index c83b2cbe1..4695e1542 100644 --- a/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTuning.java +++ b/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTuning.java @@ -26,6 +26,47 @@ private DriveTuning() { */ public static final int MAX_COILS = 512; + /** + * The coil count a "baseline" generator has — the smallest build worth calling a drive, and the + * one every quoted speed and every band figure is measured against. + */ + public static final int BASELINE_COILS = 7; + + /** + * How a generator's power grows with its SIZE: {@code base + per_coil · n^α}. Above 1, one large + * machine is worth more than several small ones of the same total volume, which is what makes + * building a bigger drive a progression rather than an addition. + * + *

    Currently 1, and the reason it is not the 2 the design derived is an invariant this file + * cannot satisfy alone. Every energy cost of a drive is proportional to its power — the window + * burst above all — while the capacitor that must pay that burst grows only with its COMPONENT + * count, capped at {@link #MAX_CAPACITOR_COMPONENTS}. So power spans {@code (512/7)^α} while the + * bank that feeds it spans a few hundred, and above α = 1 the two detach: at α = 2 a fully built + * drive's burst is roughly two hundred times a full bank, and a jump is REFUSED outright once the + * coil count passes about 35. Raising α therefore needs the capacitor economy re-derived with it, + * and no single constant does that — lifting the bank's capacity leaves the reload time absurd, + * and lowering the burst deletes the capacitor as an early-game requirement.

    + * + *

    What holds the line is the invariant, not this comment: a fully built drive must be able to + * open its own window. It is pinned by a test, so raising this number turns that test red instead + * of shipping a drive that gets slower the moment it is finished.

    + */ + public static final double COIL_POWER_EXPONENT = 1.0D; + + /** + * The drive power a generator with {@code coils} coils is worth. The one place the law lives + * — every quoted power, the baseline, the maximum and the tile that scans a real ship all read it + * here, so the exponent above cannot apply in some places and not others. + */ + public static long powerForCoils(int coils) { + int n = Math.max(0, Math.min(MAX_COILS, coils)); + if (n == 0) { + return GENERATOR_BASE_POWER; + } + double scaled = POWER_PER_COIL * Math.pow(n, COIL_POWER_EXPONENT); + return GENERATOR_BASE_POWER + (long) Math.min((double) Long.MAX_VALUE, Math.round(scaled)); + } + /** Energy the drive draws per tick while the window is held open, per unit of drive power. */ public static final double IN_FLIGHT_DRAW_PER_POWER = 0.05D; /** Energy the capacitor must dump in one moment to open the window, per unit of drive power. */ @@ -62,19 +103,39 @@ private DriveTuning() { // ─── Speed ───────────────────────────────────────────────────────────────── /** - * The drive power a "baseline" drive has, and the mass of a "baseline" hull. A ship built to - * both flies at {@link #BASELINE_SPEED_BLOCKS_PER_TICK}, and the bands that speed produces - * (seconds inside a system, an hour across a galaxy, months across the universe) are the point - * of the number — not the number itself. + * The drive power a "baseline" drive has, and the mass of a "baseline" hull. A ship built to both, + * on the baseline TIER, flies at {@link #BASELINE_SPEED_BLOCKS_PER_TICK}. + * + *

    DERIVED from {@link #BASELINE_COILS} through {@link #powerForCoils}, never written down: as a + * literal it silently stopped meaning "what a seven-coil generator is worth" the moment the power + * law gained an exponent, and the entry-level speed — a datum from play, and the one the maintainer + * has said is already acceptable — would have moved without anybody choosing to move it.

    */ - public static final long BASELINE_DRIVE_POWER = 8_000L; + public static final long BASELINE_DRIVE_POWER = powerForCoils(BASELINE_COILS); public static final long BASELINE_SHIP_MASS = 4_000L; public static final long BASELINE_SPEED_BLOCKS_PER_TICK = 1_000_000L; + /** + * What a FULLY built generator is worth — the top of what size alone can buy, and the number the + * capacitor economy has to be able to feed. Derived for the same reason as the baseline. + */ + public static final long MAX_DRIVE_POWER = powerForCoils(MAX_COILS); + // ─── Gravity dampeners ───────────────────────────────────────────────────── - /** Exit speed one powered dampener fully absorbs, in blocks per tick. */ - public static final long DAMPENER_ABSORBED_SPEED = 500_000L; + /** + * How much of a BASELINE arrival one powered dampener absorbs. A fraction and not an absolute + * speed: the balance it encodes is "two dampeners cover the ship a novice actually flies", and + * stated in blocks per tick that promise detached silently the first time the speed law moved — + * a tier multiplies every speed by its efficiency, so an absolute half of the old baseline would + * have become a rounding error on the next generation of drive. + */ + public static final double DAMPENER_ABSORBED_BASELINE_FRACTION = 0.5D; + + /** Exit speed one powered dampener fully absorbs, in blocks per tick. Derived from the fraction. */ + public static final long DAMPENER_ABSORBED_SPEED = + (long) Math.max(1d, Math.round(BASELINE_SPEED_BLOCKS_PER_TICK + * DAMPENER_ABSORBED_BASELINE_FRACTION)); /** Radius, in blocks, within which a dampener protects a crew member. */ public static final int DAMPENER_RADIUS = 12; /** Damage taken per block/tick of exit speed the dampeners failed to absorb. */ diff --git a/src/main/java/zmaster587/advancedRocketry/hyperdrive/JumpSpeed.java b/src/main/java/zmaster587/advancedRocketry/hyperdrive/JumpSpeed.java index 7afde7155..31674312d 100644 --- a/src/main/java/zmaster587/advancedRocketry/hyperdrive/JumpSpeed.java +++ b/src/main/java/zmaster587/advancedRocketry/hyperdrive/JumpSpeed.java @@ -7,10 +7,27 @@ * fly at the same speed if one of them is a freighter, and a cruiser that wants a warship's transit * time has to carry a warship's drive.

    * - *

    One constant speed covers every band the game wants — seconds inside a system, an hour across a - * galaxy, months across the universe — because the distances themselves already span nine orders of - * magnitude. Nothing piecewise is needed, and the months figure at the far end is the endgame's - * gate, not a bug to tune away.

    + *

    One constant speed does NOT cover every band — measured

    + * + *

    This class used to claim it did, on the argument that the distances already span nine orders of + * magnitude so nothing piecewise is needed. That was measured and is false. Crossing a system and + * reaching the nearest star differ by about ×5 900, and one linear coefficient cannot serve both: + * calibrated for the star, a system collapses into a single tick; calibrated for the system, the star + * costs months. The far figure was not an endgame gate, it was the same coefficient failing at the + * other end of its range.

    + * + *

    So speed has THREE inputs, and each one answers a different question:

    + *
      + *
    • power — how big the machine is. Bought with coils, spent ONCE, and it closes the first + * band.
    • + *
    • mass — what it is hauling. This is the whole reason mass stops being cosmetic: two + * ships with the same generator do not fly at the same speed if one is a freighter.
    • + *
    • {@link DriveTier} — how efficiently that power becomes speed. A whole band gap per + * generation, because by the time a tier matters the coils are already spent.
    • + *
    + * + *

    Nothing here is piecewise even so: it is one formula whose efficiency term is a property of the + * drive rather than of the distance. A leg is never classified, and no range is ever refused.

    */ public final class JumpSpeed { @@ -18,24 +35,50 @@ private JumpSpeed() { } /** - * Blocks per tick for a drive of {@code drivePower} hauling {@code shipMass}. Never below 1 — - * the transit integrator refuses a zero step, and a ship that cannot move is a softlock rather - * than a slow ship. + * Blocks per tick for a drive of {@code drivePower} and generation {@code tier} hauling + * {@code shipMass}. Never below 1 — the transit integrator refuses a zero step, and a ship that + * cannot move is a softlock rather than a slow ship. + * + *

    There is deliberately no overload that omits the tier. Which generation of drive is flying is + * something every caller KNOWS, and a default would quietly make the answer the baseline one for + * whichever call site forgot — a wrong speed being harder to notice than a missing argument.

    */ - public static long blocksPerTick(long drivePower, long shipMass) { + public static long blocksPerTick(long drivePower, long shipMass, DriveTier tier) { if (drivePower <= 0L) { return 0L; // no drive, no transit: this is refused upstream, not flown slowly } long mass = Math.max(1L, shipMass); double ratio = (drivePower / (double) DriveTuning.BASELINE_DRIVE_POWER) / (mass / (double) DriveTuning.BASELINE_SHIP_MASS); - double speed = DriveTuning.BASELINE_SPEED_BLOCKS_PER_TICK * ratio; + double efficiency = (tier == null ? DriveTier.baseline() : tier).efficiency(); + double speed = DriveTuning.BASELINE_SPEED_BLOCKS_PER_TICK * ratio * efficiency; if (speed >= Long.MAX_VALUE) { return Long.MAX_VALUE; } return Math.max(1L, (long) speed); } + /** + * Total energy a leg of {@code distanceBlocks} costs a ship of {@code shipMass} on {@code tier} — + * the in-flight draw over the whole flight. + * + *

    Drive POWER does not appear, and that is the point. Ticks go as {@code d·m/(η·P)} and + * the draw goes as {@code P}, so the two cancel exactly: a bigger drive does not change the bill + * for a trip, it changes how fast you pay it. "Size buys power, the tier buys efficiency" is + * therefore arithmetic rather than a slogan — η is the only term here that a player can improve, + * and it sits in the denominator.

    + */ + public static double routeEnergy(double distanceBlocks, long shipMass, DriveTier tier) { + if (distanceBlocks <= 0d) { + return 0d; + } + double efficiency = (tier == null ? DriveTier.baseline() : tier).efficiency(); + double massRatio = Math.max(1L, shipMass) / (double) DriveTuning.BASELINE_SHIP_MASS; + return DriveTuning.IN_FLIGHT_DRAW_PER_POWER * distanceBlocks * massRatio + * DriveTuning.BASELINE_DRIVE_POWER + / (efficiency * DriveTuning.BASELINE_SPEED_BLOCKS_PER_TICK); + } + /** * Ticks a transit of {@code distanceBlocks} takes at {@code speedBlocksPerTick}, the same way * the transit manager computes its own arrival tick — so the forecast the pilot reads before he diff --git a/src/main/java/zmaster587/advancedRocketry/hyperdrive/JumpTrigger.java b/src/main/java/zmaster587/advancedRocketry/hyperdrive/JumpTrigger.java index ca64619ac..2984c2d0f 100644 --- a/src/main/java/zmaster587/advancedRocketry/hyperdrive/JumpTrigger.java +++ b/src/main/java/zmaster587/advancedRocketry/hyperdrive/JumpTrigger.java @@ -176,7 +176,8 @@ public static Result commit(World world, BlockPos flightComputerPos, UUID shipId return new Result(Outcome.FAILED, MSG_NO_POSITION); } long speed = JumpSpeed.blocksPerTick(nav.drive().stats().drivePower(), - ShipMassProvider.massOf(world, flightComputerPos, shipId)); + ShipMassProvider.massOf(world, flightComputerPos, shipId), + nav.drive().stats().tier()); // Which world the ship must be cut out of is asked of the thing that binds cells to slots, // never remembered next to the coordinate: a slot id is minted per boot and re-used, so a diff --git a/src/main/java/zmaster587/advancedRocketry/hyperdrive/ShipDriveStats.java b/src/main/java/zmaster587/advancedRocketry/hyperdrive/ShipDriveStats.java index 829c201aa..c1995405f 100644 --- a/src/main/java/zmaster587/advancedRocketry/hyperdrive/ShipDriveStats.java +++ b/src/main/java/zmaster587/advancedRocketry/hyperdrive/ShipDriveStats.java @@ -13,33 +13,46 @@ public final class ShipDriveStats { private static final String NBT_POWER = "drivePower"; private static final String NBT_DRAW = "inFlightDraw"; private static final String NBT_BURST = "burstCost"; + private static final String NBT_TIER = "driveTier"; /** A ship with no generator at all. Every stat is zero, which is what makes it refusable. */ - public static final ShipDriveStats NONE = new ShipDriveStats(0L, 0L, 0L); + public static final ShipDriveStats NONE = + new ShipDriveStats(0L, 0L, 0L, DriveTier.baseline()); private final long drivePower; private final long inFlightDraw; private final long burstCost; + private final DriveTier tier; - public ShipDriveStats(long drivePower, long inFlightDraw, long burstCost) { + public ShipDriveStats(long drivePower, long inFlightDraw, long burstCost, DriveTier tier) { this.drivePower = Math.max(0L, drivePower); this.inFlightDraw = Math.max(0L, inFlightDraw); this.burstCost = Math.max(0L, burstCost); + this.tier = (tier == null) ? DriveTier.baseline() : tier; } /** - * The stats a generator of {@code drivePower} produces. The draw and the burst are both derived - * from the power, so a player who builds a stronger drive automatically signs up for the bigger - * capacitor and the heavier in-flight bill that come with it. + * The stats a generator of {@code drivePower} and generation {@code tier} produces. The draw and + * the burst are both derived from the power, so a player who builds a stronger drive automatically + * signs up for the bigger capacitor and the heavier in-flight bill that come with it. + * + *

    The tier is stated rather than assumed: it is the one thing about a drive that the blocks + * themselves declare, and a stats object that guessed it would fly a later generation at the + * baseline's speed with nothing to show that it had.

    */ - public static ShipDriveStats ofPower(long drivePower) { + public static ShipDriveStats ofPower(long drivePower, DriveTier tier) { long power = Math.max(0L, drivePower); if (power == 0L) { return NONE; } return new ShipDriveStats(power, (long) Math.ceil(power * DriveTuning.IN_FLIGHT_DRAW_PER_POWER), - (long) Math.ceil(power * DriveTuning.BURST_COST_PER_POWER)); + (long) Math.ceil(power * DriveTuning.BURST_COST_PER_POWER), tier); + } + + /** Which generation of drive this is — the efficiency half of the speed law. */ + public DriveTier tier() { + return tier; } /** How deep a well this drive crosses, and how fast it crosses it. */ @@ -66,16 +79,17 @@ public void writeToNBT(NBTTagCompound nbt) { nbt.setLong(NBT_POWER, drivePower); nbt.setLong(NBT_DRAW, inFlightDraw); nbt.setLong(NBT_BURST, burstCost); + nbt.setInteger(NBT_TIER, tier.ordinal()); } public static ShipDriveStats readFromNBT(NBTTagCompound nbt) { return new ShipDriveStats(nbt.getLong(NBT_POWER), nbt.getLong(NBT_DRAW), - nbt.getLong(NBT_BURST)); + nbt.getLong(NBT_BURST), DriveTier.byOrdinal(nbt.getInteger(NBT_TIER))); } @Override public String toString() { return "ShipDriveStats[power=" + drivePower + ",draw=" + inFlightDraw - + ",burst=" + burstCost + "]"; + + ",burst=" + burstCost + ",tier=" + tier + "]"; } } diff --git a/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java b/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java index 8cd3cc335..50472f887 100644 --- a/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java +++ b/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java @@ -127,7 +127,8 @@ public ShipDrive drive() { /** Blocks per tick this ship would fly at, given its drive and its hull. */ public long plannedSpeed() { return JumpSpeed.blocksPerTick(drive().stats().drivePower(), - ShipMassProvider.massOf(world, flightComputerPos, shipId)); + ShipMassProvider.massOf(world, flightComputerPos, shipId), + drive().stats().tier()); } /** How long the flight to the current target would take, in ticks. Zero without a target. */ diff --git a/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileGravityDampener.java b/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileGravityDampener.java index ff19401d5..d98145e40 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileGravityDampener.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileGravityDampener.java @@ -41,7 +41,14 @@ public boolean isPowered() { return energy.getEnergyStored() >= POWERED_THRESHOLD; } - /** The exit speed this dampener fully absorbs for everyone it covers. */ + /** + * The exit speed this dampener fully absorbs for everyone it covers. + * + *

    A fraction of a BASELINE arrival rather than an absolute number of blocks per tick: what the + * balance actually promises is "a couple of these cover the ship a novice flies", and the speed law + * multiplies every arrival by the drive's generation — so an absolute figure would have detached + * from that promise the first time a tier moved.

    + */ public long absorbedSpeed() { return DriveTuning.DAMPENER_ABSORBED_SPEED; } diff --git a/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileHyperdriveGenerator.java b/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileHyperdriveGenerator.java index 3bcb3cb81..33c8ece54 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileHyperdriveGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileHyperdriveGenerator.java @@ -8,6 +8,7 @@ import zmaster587.advancedRocketry.api.AdvancedRocketryBlocks; import zmaster587.advancedRocketry.hyperdrive.ComponentScan; +import zmaster587.advancedRocketry.hyperdrive.DriveTier; import zmaster587.advancedRocketry.hyperdrive.DriveTuning; import zmaster587.advancedRocketry.hyperdrive.ShipDriveStats; import zmaster587.advancedRocketry.tile.TileShipComponent; @@ -36,8 +37,20 @@ public class TileHyperdriveGenerator extends TileShipComponent { * number written at assembly time eventually would. */ public ShipDriveStats stats() { - return ShipDriveStats.ofPower( - DriveTuning.GENERATOR_BASE_POWER + coilCount() * DriveTuning.POWER_PER_COIL); + return ShipDriveStats.ofPower(DriveTuning.powerForCoils(coilCount()), tier()); + } + + /** + * Which generation of drive this block is. + * + *

    One generator block, one tier, so this is a property of the BLOCK and not of the build — a + * later generation is a different machine a player installs, which is what puts him back at a + * handful of coils and makes the new tier's efficiency something he feels. Only the first + * generation has a block today; the seam is here so that adding the next one is a block and a + * recipe rather than a change to the speed law.

    + */ + public DriveTier tier() { + return DriveTier.baseline(); } /** How many coils are welded to this generator. */ diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java new file mode 100644 index 000000000..dd9546760 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java @@ -0,0 +1,298 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import zmaster587.advancedRocketry.hyperdrive.DriveTier; +import zmaster587.advancedRocketry.hyperdrive.DriveTuning; +import zmaster587.advancedRocketry.hyperdrive.JumpSpeed; +import zmaster587.advancedRocketry.space.CellFrames; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.UniverseScale; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * The progression the hyperdrive family is built around: size buys POWER, the generation buys + * EFFICIENCY, and each generation owns one band of distance. + * + *

    What is pinned here is the SHAPE of the ladder and nothing about its tuning. A test that asserted + * "a full drive crosses a galaxy in 28 minutes" would fail the day anybody rebalanced, without anything + * having broken. What may be asserted is the relations that make the ladder a ladder:

    + *
      + *
    • a full build of each generation crosses ITS OWN band in the same time — that is what "one tier + * per band" means, and it holds at any exponent and any baseline speed;
    • + *
    • a route's total energy does not depend on drive POWER — the property that makes "the tier buys + * efficiency" arithmetic rather than a slogan;
    • + *
    • nothing is ever refused for being far;
    • + *
    • a fully built drive can open its own window — the invariant that decides how far the + * power law may be bent.
    • + *
    + */ +public class DriveLadderTest { + + /** Distances are quoted in light years and flown in blocks; this is the one conversion. */ + private static double blocksForLightYears(double lightYears) { + return lightYears * (double) AstronomicalBodyHelper.BLOCKS_PER_LIGHT_YEAR; + } + + /** A fully built generator of {@code tier}, hauling the baseline hull. */ + private static long fullBuildSpeed(DriveTier tier) { + return JumpSpeed.blocksPerTick(DriveTuning.MAX_DRIVE_POWER, + DriveTuning.PLACEHOLDER_SHIP_MASS, tier); + } + + // ── the ladder ──────────────────────────────────────────────────────────── + + @Test + public void aFullBuildOfEachGenerationCrossesITSOWNBandInTheSameTime() { + // THE defining property, and the reason a generation's efficiency is derived rather than + // chosen: a tier's efficiency IS the gap between its band and the previous one, so a player who + // has finished building one generation and then installs the next stands in the same relation to + // the new band as he did to the old. Independent of the exponent, the baseline speed and the + // hull mass — which is exactly why it is the thing worth pinning. + long interstellar = JumpSpeed.transitTicks( + blocksForLightYears(DriveTier.INTERSTELLAR.bandLightYears()), + fullBuildSpeed(DriveTier.INTERSTELLAR)); + long galactic = JumpSpeed.transitTicks( + blocksForLightYears(DriveTier.GALACTIC.bandLightYears()), + fullBuildSpeed(DriveTier.GALACTIC)); + + System.out.println(String.format( + "full build: interstellar band %.2f ly -> %d ticks (%.1f min); galactic band %.0f ly" + + " -> %d ticks (%.1f min)", + DriveTier.INTERSTELLAR.bandLightYears(), interstellar, interstellar / 1200d, + DriveTier.GALACTIC.bandLightYears(), galactic, galactic / 1200d)); + + assertTrue("a band that takes no time at all is not a flight", interstellar > 0L); + double ratio = galactic / (double) interstellar; + assertEquals("each generation must stand in the same relation to its own band as the previous" + + " one does to its own; the two crossings came out " + interstellar + " vs " + + galactic + " ticks", 1d, ratio, 0.01d); + } + + @Test + public void theGalacticGenerationIsWorthMoreThanEveryCoilOnTheShip() { + // A generation is only felt as an upgrade if it beats a MAXED build of the previous one, because + // installing it puts the player back at a handful of coils. This is the condition that decides + // how many tiers exist at all: a band gap smaller than what iron already buys is a tier that + // would make its owner slower. + double boughtBySize = DriveTuning.MAX_DRIVE_POWER / (double) DriveTuning.BASELINE_DRIVE_POWER; + double boughtByTier = DriveTier.GALACTIC.efficiency(); + System.out.println(String.format( + "size buys x%.0f (%d coils -> %d power); the galactic generation buys x%.0f", + boughtBySize, DriveTuning.MAX_COILS, DriveTuning.MAX_DRIVE_POWER, boughtByTier)); + assertTrue("a fresh galactic drive (" + (long) boughtByTier + "x) must beat a maxed" + + " interstellar one (" + (long) boughtBySize + "x), or installing it is a" + + " downgrade wearing an upgrade's name", + boughtByTier > boughtBySize); + } + + @Test + public void aRoutesENERGYdoesNotDependOnHowBigTheDriveIs() { + // The property that makes "size buys power, the tier buys efficiency" literal: ticks go as + // d.m/(eta.P) and the in-flight draw goes as P, so power cancels exactly. A bigger drive does + // not change the bill for a trip, only how fast it is paid. If this ever stops holding, size + // has started buying part of the efficiency and the two knobs have blurred into one. + double distance = blocksForLightYears(10d); + double small = JumpSpeed.routeEnergy(distance, DriveTuning.BASELINE_SHIP_MASS, + DriveTier.INTERSTELLAR); + double large = JumpSpeed.routeEnergy(distance, DriveTuning.BASELINE_SHIP_MASS, + DriveTier.INTERSTELLAR); + assertEquals("route energy must not read drive power at all", small, large, 0d); + + // And the same claim measured THROUGH the speed law rather than off the closed form, because the + // closed form is where the cancellation could be true while the flight disagreed. + assertEquals("the closed form and the flight must agree on the bill", + flownEnergy(distance, DriveTuning.BASELINE_DRIVE_POWER, DriveTier.INTERSTELLAR), + flownEnergy(distance, DriveTuning.MAX_DRIVE_POWER, DriveTier.INTERSTELLAR), + flownEnergy(distance, DriveTuning.BASELINE_DRIVE_POWER, DriveTier.INTERSTELLAR) + * 0.02d); + + // A later generation is CHEAPER per unit distance — that is what efficiency means. + assertTrue("a galactic drive must cost less energy for the same leg", + JumpSpeed.routeEnergy(distance, DriveTuning.BASELINE_SHIP_MASS, DriveTier.GALACTIC) + < small); + } + + /** The bill as actually flown: the per-tick draw times the ticks the speed law produces. */ + private static double flownEnergy(double distance, long drivePower, DriveTier tier) { + long speed = JumpSpeed.blocksPerTick(drivePower, DriveTuning.BASELINE_SHIP_MASS, tier); + long ticks = JumpSpeed.transitTicks(distance, speed); + return ticks * drivePower * DriveTuning.IN_FLIGHT_DRAW_PER_POWER; + } + + @Test + public void aBaselineDriveAimedAcrossInterstellarSpaceIsNOTrefused() { + // A generation is a coefficient, never a licence. The first drive a player builds, aimed at + // something absurdly far, departs and takes what it takes — the barrier is then life support + // and generation over that duration, which are real systems, rather than a red message. + long speed = JumpSpeed.blocksPerTick(DriveTuning.BASELINE_DRIVE_POWER, + DriveTuning.PLACEHOLDER_SHIP_MASS, DriveTier.INTERSTELLAR); + long ticks = JumpSpeed.transitTicks( + blocksForLightYears(DriveTier.GALACTIC.bandLightYears()), speed); + + assertTrue("a baseline drive must still have a speed", speed > 0L); + assertTrue("and a finite, statable duration for a trip it has no business making: " + ticks, + ticks > 0L && ticks < Long.MAX_VALUE); + System.out.println("a baseline drive crosses a galaxy in " + ticks + " ticks (" + + String.format("%.1f", ticks / 1728000d) + " in-game months) - unreasonable, not" + + " impossible"); + } + + // ── the invariant that bounds the power law ──────────────────────────────── + + @Test + public void aFULLYBUILTdriveMustBeAbleToOpenItsOwnWindow() { + // The invariant nobody had written down, and it is what decides how far the power law may be + // bent. Every energy cost of a drive is proportional to its power — the window burst above all — + // while the capacitor that pays that burst grows only with its COMPONENT count, which is capped. + // So the two ranges have to be checked against each other: a drive whose burst outruns any bank + // a player can build is REFUSED at the gate, which means growing it past some coil count makes + // it useless. That is a lock, and a lock is the one thing a cost may not become. + long fullBank = DriveTuning.CAPACITOR_BASE_CAPACITY + + (long) DriveTuning.MAX_CAPACITOR_COMPONENTS * DriveTuning.CAPACITY_PER_CELL; + long fullBurst = (long) Math.ceil(DriveTuning.MAX_DRIVE_POWER + * DriveTuning.BURST_COST_PER_POWER); + + System.out.println(String.format( + "at exponent %.2f a full drive is %d power, burst %d, against a full bank of %d" + + " (margin x%.2f)", + DriveTuning.COIL_POWER_EXPONENT, DriveTuning.MAX_DRIVE_POWER, fullBurst, fullBank, + fullBank / (double) fullBurst)); + + assertTrue("a fully built drive cannot open its window: burst " + fullBurst + + " against a full capacitor bank of " + fullBank + + ". Raising COIL_POWER_EXPONENT needs the capacitor economy re-derived with" + + " it — see that constant's javadoc for the measured collision.", + fullBurst <= fullBank); + } + + @Test + public void aBaselineDriveStillNEEDSacapacitorBank() { + // The other end of the same bound, and the reason it cannot be fixed by simply making the burst + // cheaper: a novice's window must cost more than the controller block holds on its own, or the + // capacitor stops being something he has to build. + long baselineBurst = (long) Math.ceil(DriveTuning.BASELINE_DRIVE_POWER + * DriveTuning.BURST_COST_PER_POWER); + assertTrue("a baseline window costs " + baselineBurst + ", which a bare controller (" + + DriveTuning.CAPACITOR_BASE_CAPACITY + ") already covers — the capacitor has" + + " stopped being a requirement", + baselineBurst > DriveTuning.CAPACITOR_BASE_CAPACITY); + } + + // ── the two knobs, and the derived numbers that must not detach ──────────── + + @Test + public void theBaselineIsWHATASEVENCOILGENERATORISWORTH_notALiteral() { + // The entry-level speed is a datum from play and must not move unless somebody moves it. It did + // move once, silently: the baseline power was a literal that stopped being the seven-coil figure + // the moment the power law gained an exponent. + assertEquals("the baseline power must BE the baseline build's power", + DriveTuning.powerForCoils(DriveTuning.BASELINE_COILS), + DriveTuning.BASELINE_DRIVE_POWER); + assertEquals("so a baseline ship flies at exactly the baseline speed", + DriveTuning.BASELINE_SPEED_BLOCKS_PER_TICK, + JumpSpeed.blocksPerTick(DriveTuning.BASELINE_DRIVE_POWER, + DriveTuning.BASELINE_SHIP_MASS, DriveTier.INTERSTELLAR)); + } + + @Test + public void aDampenerAbsorbsAFRACTIONofABaselineArrival() { + // Expressed as a ratio, so what it promises — "a couple of these cover the ship a novice + // flies" — survives the speed law moving. As an absolute it became a rounding error on the next + // generation of drive the first time a tier multiplied every arrival. + long baseline = JumpSpeed.blocksPerTick(DriveTuning.BASELINE_DRIVE_POWER, + DriveTuning.BASELINE_SHIP_MASS, DriveTier.INTERSTELLAR); + long absorbed = DriveTuning.DAMPENER_ABSORBED_SPEED; + int needed = (int) Math.ceil(baseline / (double) absorbed); + + System.out.println("a baseline arrival of " + baseline + " needs " + needed + " dampener(s) at " + + absorbed + " each"); + assertEquals("one dampener must absorb the configured fraction of a baseline arrival", + DriveTuning.DAMPENER_ABSORBED_BASELINE_FRACTION, + absorbed / (double) baseline, 1e-9d); + assertTrue("and a baseline arrival must need more than one, or the dampener is free", + needed > 1); + } + + @Test + public void theGalacticEfficiencyISTheBandGap_notANumberSomebodyPicked() { + // Written as a literal it would be a number nobody could check, and one that silently stopped + // meaning "one band" the first time the star separation or the galaxy size was retuned. It rests + // on exactly two constants, and this is what says so. + double expected = 2d * UniverseScale.REFERENCE_GALAXY_RADIUS_LY + / UniverseScale.MEAN_STAR_SEPARATION_LY; + assertEquals("the galactic generation's efficiency must BE the star -> galaxy gap", expected, + DriveTier.GALACTIC.efficiency(), 1e-9d); + assertEquals("the baseline generation is the unit every other is quoted against", 1d, + DriveTier.INTERSTELLAR.efficiency(), 0d); + } + + // ── measured through the real generator, over the same 20 seeds ──────────── + + @Test + public void theInterstellarBandIsWhatTheGeneratorActuallyProduces() { + // The band figures above are arithmetic on two constants; this is the same span measured through + // the real generator, over the same 20 seeds the leg reading uses. If the generator's actual + // nearest-neighbour distance drifts away from the separation the ladder is derived against, the + // tiers are no longer aimed at the bands they are named for. + final double TOLERANCE_FACTOR = 2d; + + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(GalaxyGenConfig.defaults()); + long stride = 4L * GalaxyGenConfig.DEFAULT_MIN_SPACING; + List legs = new ArrayList<>(); + for (long seed = 1L; seed <= 20L; seed++) { + Map found = gen.systemsInRegion(seed, + cell(-stride, -stride, -stride), cell(stride, stride, stride)); + GalacticCoord home = nearestTo(found.keySet(), cell(0L, 0L, 0L)); + GalacticCoord neighbour = home == null ? null : nearestTo(found.keySet(), home); + if (neighbour == null) { + continue; + } + legs.add(CellFrames.STATIC.distanceBetween(home, neighbour, 0L) + / (double) AstronomicalBodyHelper.BLOCKS_PER_LIGHT_YEAR); + } + Collections.sort(legs); + assertTrue("no seed produced a pair of systems to measure a leg from", !legs.isEmpty()); + + double median = legs.get(legs.size() / 2); + double declared = DriveTier.INTERSTELLAR.bandLightYears(); + System.out.println(String.format( + "interstellar band: declared %.2f ly, measured median %.2f ly over %d seeds" + + " (min %.2f, max %.2f)", + declared, median, legs.size(), legs.get(0), legs.get(legs.size() - 1))); + + assertTrue("the measured leg " + String.format("%.2f", median) + " ly is not the band the" + + " interstellar generation is named for (" + String.format("%.2f", declared) + + " ly) within a factor of " + TOLERANCE_FACTOR, + median >= declared / TOLERANCE_FACTOR && median <= declared * TOLERANCE_FACTOR); + } + + private static GalacticCoord cell(long sx, long sy, long sz) { + return GalacticCoord.ofSectorLocal(sx, sy, sz, 0L, 0L, 0L); + } + + private static GalacticCoord nearestTo(java.util.Collection cells, + GalacticCoord from) { + GalacticCoord best = null; + double bestDist = Double.MAX_VALUE; + for (GalacticCoord c : cells) { + double d = CellFrames.STATIC.distanceBetween(from, c, 0L); + if (d > 0d && d < bestDist) { + bestDist = d; + best = c; + } + } + return best; + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/HyperdriveStatsTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/HyperdriveStatsTest.java index 265b87934..8de85b739 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/HyperdriveStatsTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/HyperdriveStatsTest.java @@ -10,6 +10,7 @@ import zmaster587.advancedRocketry.hyperdrive.ComponentScan; import zmaster587.advancedRocketry.hyperdrive.DampenerField; +import zmaster587.advancedRocketry.hyperdrive.DriveTier; import zmaster587.advancedRocketry.hyperdrive.DriveTuning; import zmaster587.advancedRocketry.hyperdrive.JumpSpeed; import zmaster587.advancedRocketry.hyperdrive.ShipDriveStats; @@ -84,8 +85,8 @@ public void theScanIsBoundedAndSaysWhenItStopped() { @Test public void aBiggerGeneratorIsABetterGenerator() { - ShipDriveStats small = ShipDriveStats.ofPower(2_000L); - ShipDriveStats large = ShipDriveStats.ofPower(20_000L); + ShipDriveStats small = ShipDriveStats.ofPower(2_000L, DriveTier.baseline()); + ShipDriveStats large = ShipDriveStats.ofPower(20_000L, DriveTier.baseline()); assertTrue("more power crosses deeper wells and crosses them faster", large.drivePower() > small.drivePower()); @@ -100,12 +101,12 @@ public void aShipWithNoGeneratorHasNoDrive() { assertFalse(ShipDriveStats.NONE.present()); assertEquals(0L, ShipDriveStats.NONE.burstCost()); assertFalse("a generator of zero power is the same thing as no generator", - ShipDriveStats.ofPower(0L).present()); + ShipDriveStats.ofPower(0L, DriveTier.baseline()).present()); } @Test public void driveStatsSurviveAnNbtRoundTrip() { - ShipDriveStats original = ShipDriveStats.ofPower(12_345L); + ShipDriveStats original = ShipDriveStats.ofPower(12_345L, DriveTier.baseline()); NBTTagCompound nbt = new NBTTagCompound(); original.writeToNBT(nbt); @@ -120,16 +121,16 @@ public void driveStatsSurviveAnNbtRoundTrip() { @Test public void aHeavierShipOnTheSameDriveIsSlower() { - long light = JumpSpeed.blocksPerTick(DriveTuning.BASELINE_DRIVE_POWER, 1_000L); - long heavy = JumpSpeed.blocksPerTick(DriveTuning.BASELINE_DRIVE_POWER, 100_000L); + long light = JumpSpeed.blocksPerTick(DriveTuning.BASELINE_DRIVE_POWER, 1_000L, DriveTier.baseline()); + long heavy = JumpSpeed.blocksPerTick(DriveTuning.BASELINE_DRIVE_POWER, 100_000L, DriveTier.baseline()); assertTrue("mass is what makes a cruiser need a cruiser's drive", heavy < light); } @Test public void aStrongerDriveOnTheSameHullIsFaster() { - long weak = JumpSpeed.blocksPerTick(1_000L, DriveTuning.BASELINE_SHIP_MASS); - long strong = JumpSpeed.blocksPerTick(50_000L, DriveTuning.BASELINE_SHIP_MASS); + long weak = JumpSpeed.blocksPerTick(1_000L, DriveTuning.BASELINE_SHIP_MASS, DriveTier.baseline()); + long strong = JumpSpeed.blocksPerTick(50_000L, DriveTuning.BASELINE_SHIP_MASS, DriveTier.baseline()); assertTrue(strong > weak); } @@ -139,14 +140,14 @@ public void evenAnAbsurdlyOverloadedShipStillMoves() { // The transit integrator refuses a zero step, so a ship that computes to "slower than one // block per tick" must round up to one rather than becoming a permanent fixture of // hyperspace. - long speed = JumpSpeed.blocksPerTick(1L, Long.MAX_VALUE / 2L); + long speed = JumpSpeed.blocksPerTick(1L, Long.MAX_VALUE / 2L, DriveTier.baseline()); assertTrue("a crawling ship is a slow ship, not a stuck one", speed >= 1L); } @Test public void aShipWithNoDriveHasNoSpeedAtAll() { - assertEquals("refused upstream, not flown slowly", 0L, JumpSpeed.blocksPerTick(0L, 100L)); + assertEquals("refused upstream, not flown slowly", 0L, JumpSpeed.blocksPerTick(0L, 100L, DriveTier.baseline())); } @Test diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java index 296e09bcc..94f7f57f8 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; +import zmaster587.advancedRocketry.hyperdrive.DriveTier; import zmaster587.advancedRocketry.hyperdrive.DriveTuning; import zmaster587.advancedRocketry.hyperdrive.JumpSpeed; import zmaster587.advancedRocketry.space.CellFrames; @@ -36,7 +37,8 @@ public class InterstellarLegDistanceTest { /** A baseline drive hauling the placeholder hull: the reference ship every band is quoted for. */ private static final long BASELINE_SPEED = - JumpSpeed.blocksPerTick(DriveTuning.BASELINE_DRIVE_POWER, DriveTuning.PLACEHOLDER_SHIP_MASS); + JumpSpeed.blocksPerTick(DriveTuning.BASELINE_DRIVE_POWER, DriveTuning.PLACEHOLDER_SHIP_MASS, + DriveTier.baseline()); private static GalacticCoord cell(long sx, long sy, long sz) { return GalacticCoord.ofSectorLocal(sx, sy, sz, 0L, 0L, 0L); From bbbfb83fd47a68261ed9bb8774310bd74467ce53 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 19:49:35 +0300 Subject: [PATCH 33/42] fix: the jump capacitor is filled by the ship, not by the clock - it becomes a Forge Energy receiver like every other machine - heat sinks bound what the buffer accepts; they make nothing - extraction is refused: a jump bank is not the ship's battery - the cooldown is a forecast at full inflow, and says so - retracts the unloaded-cell fairness the free charge bought --- .../command/test/TestProbeCommand.java | 10 +- .../hyperdrive/CapacitorCharge.java | 60 ++---- .../hyperdrive/DriveTuning.java | 23 +- .../hyperdrive/ShipDrive.java | 24 ++- .../navigation/ShipNavigation.java | 2 +- .../tile/TileAdvancedFlightComputer.java | 2 +- .../tile/TileNavigationComputer.java | 4 +- .../tile/hyperdrive/TileJumpCapacitor.java | 174 +++++++++++---- .../VSJumpDriveFixtureBoardingE2ETest.java | 14 +- .../test/unit/CapacitorChargeTest.java | 201 ++++++++++++++---- 10 files changed, 361 insertions(+), 153 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 567ee5336..8528dbc64 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -2738,12 +2738,12 @@ private void handleDrive(MinecraftServer server, ICommandSender sender, String[] for (zmaster587.advancedRocketry.tile.hyperdrive.TileJumpCapacitor capacitor : drive.capacitors()) { if (full) { - capacitor.fill(now); + capacitor.fill(); } else { - capacitor.discharge(capacitor.chargeAt(now), now); + capacitor.discharge(capacitor.charge()); } } - send(sender, "{\"ok\":true,\"charge\":" + drive.capacitorCharge(now) + "}"); + send(sender, "{\"ok\":true,\"charge\":" + drive.capacitorCharge() + "}"); return; } if ("arm".equalsIgnoreCase(verb)) { @@ -2787,8 +2787,8 @@ private void handleDrive(MinecraftServer server, ICommandSender sender, String[] info.put("burstCost", stats.burstCost()); info.put("capacitors", drive.capacitors().size()); info.put("capacity", drive.capacitorCapacity()); - info.put("charge", drive.capacitorCharge(now)); - info.put("cooldownTicks", drive.cooldownTicks(now)); + info.put("charge", drive.capacitorCharge()); + info.put("cooldownTicks", drive.cooldownTicks()); info.put("emitters", drive.emitters().size()); info.put("dampeners", drive.dampeners().size()); info.put("poweredDampeners", drive.poweredDampenerPositions().size()); diff --git a/src/main/java/zmaster587/advancedRocketry/hyperdrive/CapacitorCharge.java b/src/main/java/zmaster587/advancedRocketry/hyperdrive/CapacitorCharge.java index bce88a9e8..e5d077cde 100644 --- a/src/main/java/zmaster587/advancedRocketry/hyperdrive/CapacitorCharge.java +++ b/src/main/java/zmaster587/advancedRocketry/hyperdrive/CapacitorCharge.java @@ -1,17 +1,21 @@ package zmaster587.advancedRocketry.hyperdrive; /** - * The capacitor's charge, computed rather than accumulated. + * How long a jump bank takes to reach a level — the cooldown a pilot is quoted, and nothing else. * - *

    Nothing here ever ticks. The charge is a closed form of the world clock — {@code charge(t) = - * min(capacity, c0 + rate·(t − since))} — so a capacitor aboard a ship parked in an unloaded cell, - * or one that spent a month in hyperspace, is exactly as charged as one that sat in a loaded chunk - * the whole time. Only {@code c0} and {@code since} persist, and they only change when something - * really happens to the capacitor: a burst, or a rebuild.

    + *

    This class used to BE the charge, and that was the defect. It held a closed form of the + * world clock, {@code charge(t) = min(capacity, c0 + rate·(t − since))}, so a capacitor stored no + * energy: its level was arithmetic over elapsed ticks and the rate was conjured by welding heat sinks + * on. The hyperdrive's largest single cost — the window burst, twenty times the drive's power — was + * therefore free, paid for in wall-clock time rather than in generation. The bank is now a real Forge + * Energy receiver fed by the ship (see {@code TileJumpCapacitor}), and what is left here is the one + * thing that was never wrong: turning a deficit and a rate into a number of ticks.

    * - *

    The cooldown a pilot feels falls out of the same form and needs no timer of its own: after a - * burst the capacitor is empty, so the reload is however long {@code charge(t)} takes to climb back - * to the next burst's cost.

    + *

    What that number IS has changed with it. It used to be a prediction, because the rate was a + * property of the capacitor and could not be missed. It is now a best case: the rate is the + * bank's own accept limit, and whether the ship's power plant actually delivers it is the plant's + * business. A forecast that says "at full inflow" is honest; the same number presented as a promise + * would be the free energy coming back as a lie about time.

    */ public final class CapacitorCharge { @@ -19,43 +23,23 @@ private CapacitorCharge() { } /** - * The charge at {@code now}. Clamped at both ends: never below zero, never above capacity, and - * never advanced by a clock that has run backwards (which a restored world can do). + * Ticks from now until a bank holding {@code current} of {@code capacity} reaches {@code needed}, + * fed at {@code ratePerTick}. Zero means "already"; {@code -1} means never, because the bank + * cannot hold that much however long anybody waits. */ - public static long at(long baseCharge, long since, long chargeRate, long capacity, long now) { - long cap = Math.max(0L, capacity); - long base = Math.min(cap, Math.max(0L, baseCharge)); - long elapsed = now - since; - if (elapsed <= 0L || chargeRate <= 0L) { - return base; - } - long gained; - long rate = Math.max(0L, chargeRate); - if (rate != 0L && elapsed > (Long.MAX_VALUE - base) / rate) { - gained = Long.MAX_VALUE - base; // a months-long absence overflows a naive multiply - } else { - gained = rate * elapsed; - } - return Math.min(cap, base + gained); - } - - /** - * How many ticks from {@code now} until the charge reaches {@code needed}, or {@code -1} when it - * never will because the capacitor is too small to hold that much. Zero means "already". - */ - public static long ticksUntil(long baseCharge, long since, long chargeRate, long capacity, - long now, long needed) { + public static long ticksToReach(long current, long capacity, long ratePerTick, long needed) { long cap = Math.max(0L, capacity); if (needed <= 0L) { return 0L; } - long current = at(baseCharge, since, chargeRate, cap, now); - if (current >= needed) { + long have = Math.min(cap, Math.max(0L, current)); + if (have >= needed) { return 0L; } - if (needed > cap || chargeRate <= 0L) { + if (needed > cap || ratePerTick <= 0L) { return -1L; // no amount of waiting gets there } - return (needed - current + chargeRate - 1L) / chargeRate; + long deficit = needed - have; + return (deficit + ratePerTick - 1L) / ratePerTick; } } diff --git a/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTuning.java b/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTuning.java index 4695e1542..32faac378 100644 --- a/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTuning.java +++ b/src/main/java/zmaster587/advancedRocketry/hyperdrive/DriveTuning.java @@ -89,14 +89,25 @@ public static long powerForCoils(int coils) { public static final long CAPACITOR_BASE_CAPACITY = 20_000L; /** Charge each capacitor cell adds. */ public static final long CAPACITY_PER_CELL = 100_000L; - /** Charge per tick the controller recovers on its own. */ - public static final long CAPACITOR_BASE_CHARGE_RATE = 10L; + + /** + * How much charge per tick the controller can ACCEPT on its own — a throughput limit, never a + * supply. The energy comes from the ship's own generation; this is only how fast the buffer will + * swallow it. + * + *

    It was {@code CAPACITOR_BASE_CHARGE_RATE} and it meant the opposite: joules the block + * recovered by itself, which made the largest cost in this family free. The rename is the whole + * correction — a rate constant standing in for an absent power plant will absorb any amount of + * tuning and never come right.

    + */ + public static final long CAPACITOR_BASE_ACCEPT_RATE = 10L; /** - * Charge per tick each heat sink adds. Cooling does not get a mechanism of its own: a sink - * raises the rate at which the capacitor refills, and the reload time — the cooldown a pilot - * actually feels — is {@code burstCost / chargeRate} with no timer to persist. + * How much more charge per tick each heat sink lets the bank accept. Cooling does not get a + * mechanism of its own: a sink is what allows a large inflow to be swallowed without cooking, so + * the cooldown a pilot feels is his reactors' output against this ceiling — with no timer to + * persist and no energy created anywhere. */ - public static final long CHARGE_RATE_PER_SINK = 40L; + public static final long ACCEPT_RATE_PER_SINK = 40L; /** How many capacitor components (cells + sinks) one controller will count. */ public static final int MAX_CAPACITOR_COMPONENTS = 256; diff --git a/src/main/java/zmaster587/advancedRocketry/hyperdrive/ShipDrive.java b/src/main/java/zmaster587/advancedRocketry/hyperdrive/ShipDrive.java index 11d8c4f06..25eaf1ff8 100644 --- a/src/main/java/zmaster587/advancedRocketry/hyperdrive/ShipDrive.java +++ b/src/main/java/zmaster587/advancedRocketry/hyperdrive/ShipDrive.java @@ -99,11 +99,11 @@ public ShipDriveStats stats() { return gen == null ? ShipDriveStats.NONE : gen.stats(); } - /** Charge available across every connected capacitor at {@code now}. */ - public long capacitorCharge(long now) { + /** Charge available across every connected capacitor. */ + public long capacitorCharge() { long total = 0L; for (TileJumpCapacitor capacitor : capacitors()) { - total += capacitor.chargeAt(now); + total += capacitor.charge(); } return total; } @@ -118,21 +118,23 @@ public long capacitorCapacity() { } /** - * Ticks until the bank can open a window again, or {@code -1} when it never can. This is the - * cooldown, and it is entirely a consequence of what the player built. + * Ticks until the bank can open a window again if the ship feeds it at the bank's full accept + * rate, or {@code -1} when it never can. A BEST CASE: the energy comes from the ship's own + * generation, so a pilot who has under-built his reactors waits longer than this says. It is still + * entirely a consequence of what the player built — now of two things he built rather than one. */ - public long cooldownTicks(long now) { + public long cooldownTicks() { long needed = stats().burstCost(); if (needed <= 0L) { return -1L; } long best = -1L; - long charge = capacitorCharge(now); + long charge = capacitorCharge(); if (charge >= needed) { return 0L; } for (TileJumpCapacitor capacitor : capacitors()) { - long ticks = capacitor.ticksUntil(needed, now); + long ticks = capacitor.ticksUntilAtFullInflow(needed); if (ticks < 0L) { continue; } @@ -149,7 +151,7 @@ public long cooldownTicks(long now) { */ public boolean fireBurst(long now) { long needed = stats().burstCost(); - if (needed <= 0L || capacitorCharge(now) < needed) { + if (needed <= 0L || capacitorCharge() < needed) { return false; } long remaining = needed; @@ -157,9 +159,9 @@ public boolean fireBurst(long now) { if (remaining <= 0L) { break; } - long available = capacitor.chargeAt(now); + long available = capacitor.charge(); long take = Math.min(available, remaining); - if (take > 0L && capacitor.discharge(take, now) == take) { + if (take > 0L && capacitor.discharge(take) == take) { remaining -= take; } } diff --git a/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java b/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java index 50472f887..f3f48b878 100644 --- a/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java +++ b/src/main/java/zmaster587/advancedRocketry/navigation/ShipNavigation.java @@ -93,7 +93,7 @@ public long capacitorCapacity() { @Override public long capacitorCharge() { - return drive().capacitorCharge(SpaceSubsystem.spaceClock()); + return drive().capacitorCharge(); } @Override diff --git a/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java b/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java index 3ec1c338d..7cb80fc1a 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/TileAdvancedFlightComputer.java @@ -885,7 +885,7 @@ private void refreshHudDrive(long now) { } long capacity = drive.capacitorCapacity(); hudDriveCharge = capacity <= 0 ? 0f - : (float) Math.min(1.0, (double) drive.capacitorCharge(now) / (double) capacity); + : (float) Math.min(1.0, (double) drive.capacitorCharge() / (double) capacity); zmaster587.advancedRocketry.navigation.ShipNavigation nav = new zmaster587.advancedRocketry.navigation.ShipNavigation(world, getPos(), shipId); zmaster587.advancedRocketry.tile.TileNavigationComputer computer = nav.findNavComputer(); diff --git a/src/main/java/zmaster587/advancedRocketry/tile/TileNavigationComputer.java b/src/main/java/zmaster587/advancedRocketry/tile/TileNavigationComputer.java index a9c355cc6..f2a30e888 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/TileNavigationComputer.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/TileNavigationComputer.java @@ -690,7 +690,7 @@ private String computeForecast() { out.append(LibVulpes.proxy.getLocalizedString("msg.navcomputer.drivepower")) .append(' ').append(stats.drivePower()).append('\n'); out.append(LibVulpes.proxy.getLocalizedString("msg.navcomputer.burst")) - .append(' ').append(drive.capacitorCharge(now)) + .append(' ').append(drive.capacitorCharge()) .append('/').append(stats.burstCost()).append('\n'); // What the bank IS, beside what is in it. A charge of 0/40000 reads as "wait" whether // the ship has no capacitor at all or one that can never hold that much, and a pilot who @@ -698,7 +698,7 @@ private String computeForecast() { out.append(LibVulpes.proxy.getLocalizedString("msg.navcomputer.capacitors")) .append(' ').append(drive.capacitors().size()) .append(" (").append(drive.capacitorCapacity()).append(")\n"); - long cooldown = drive.cooldownTicks(now); + long cooldown = drive.cooldownTicks(); if (cooldown > 0L) { out.append(LibVulpes.proxy.getLocalizedString("msg.navcomputer.cooldown")) .append(' ').append(cooldown / 20L).append("s\n"); diff --git a/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileJumpCapacitor.java b/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileJumpCapacitor.java index a6b16e9b7..deedcd7d4 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileJumpCapacitor.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/hyperdrive/TileJumpCapacitor.java @@ -1,8 +1,13 @@ package zmaster587.advancedRocketry.tile.hyperdrive; +import javax.annotation.Nullable; + import net.minecraft.block.Block; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumFacing; import net.minecraft.util.math.BlockPos; +import net.minecraftforge.energy.CapabilityEnergy; +import net.minecraftforge.energy.IEnergyStorage; import zmaster587.advancedRocketry.api.AdvancedRocketryBlocks; import zmaster587.advancedRocketry.hyperdrive.CapacitorCharge; @@ -15,27 +20,73 @@ * *

    A jump does not need a lot of energy over time so much as a great deal of it in one instant, * which is why this is a separate machine standing beside the generator rather than a bigger battery - * inside it. Cells decide how much it holds; heat sinks decide how fast it recovers — and the - * cooldown a pilot feels between jumps is nothing but that recovery, so there is no timer here and - * no thermal state to keep.

    + * inside it. Cells decide how much it holds; heat sinks decide how fast it can ACCEPT charge. + * The cooldown a pilot feels between jumps is how long his own power plant takes to refill it, so + * there is no timer here and no thermal state to keep.

    + * + *

    The energy comes from the SHIP — it is not manufactured here

    + * + *

    This is a Forge Energy receiver like any other machine: reactors, solar arrays and cables push + * into it. It refuses EXTRACTION through the capability on purpose — a jump bank is not a battery for + * the rest of the vessel, and only the drive's own burst may take from it. So the biggest single cost + * in the hyperdrive family is paid for out of generation the player built, which is what makes + * "sustained generation aboard" a pressure rather than a sentence in a design document.

    * - *

    It never ticks. The charge is arithmetic over the SPACE clock, so a capacitor aboard a - * ship that spent a month in hyperspace, or parked in a cell nobody loaded, is exactly as charged as - * one that sat in a busy chunk the whole time. Only the level at the last real event, and when that - * event was, are ever written down.

    + *

    RETRACTED, and the retraction is the point of this class's history. It used to hold no + * energy at all: the level was a closed form of the world clock, + * {@code min(capacity, c0 + rate·(t − since))}, with the rate conjured by welding heat sinks on. That + * bought one property — a capacitor aboard a ship in an unloaded cell was exactly as charged as one in + * a busy chunk — and the property was only defensible while the energy was FREE. An unloaded ship's + * reactors are not running either, so charging through an absence was creating energy from nothing a + * second time, more quietly. The fairness it was reaching for belongs to whatever powers the ship, not + * to its buffer.

    */ public class TileJumpCapacitor extends TileShipComponent { static final String KIND_CELL = "cell"; static final String KIND_SINK = "sink"; - private static final String NBT_BASE_CHARGE = "capBaseCharge"; - private static final String NBT_SINCE = "capSince"; + private static final String NBT_CHARGE = "capCharge"; + + /** What is actually in the bank, in Forge Energy units. Never above {@link #capacity()}. */ + private long charge; + + /** + * The face the ship's grid pushes into. Capacity and accept rate are read from the BUILD on every + * call rather than fixed at construction: a cell pulled out mid-flight has to make the bank + * smaller the moment it is pulled, exactly as a coil pulled out makes the ship slower. + */ + private final IEnergyStorage port = new IEnergyStorage() { + @Override + public int receiveEnergy(int maxReceive, boolean simulate) { + return (int) acceptCharge(maxReceive, simulate); + } + + @Override + public int extractEnergy(int maxExtract, boolean simulate) { + return 0; // a jump bank is not the ship's battery; only the drive's burst takes from it + } - /** The charge as of {@link #since} — the level at the last thing that actually happened. */ - private long baseCharge; - /** Space-clock tick of that event. Everything after it is computed, never accumulated. */ - private long since; + @Override + public int getEnergyStored() { + return (int) Math.min(Integer.MAX_VALUE, charge); + } + + @Override + public int getMaxEnergyStored() { + return (int) Math.min(Integer.MAX_VALUE, capacity()); + } + + @Override + public boolean canExtract() { + return false; + } + + @Override + public boolean canReceive() { + return true; + } + }; /** How much this bank holds when full. */ public long capacity() { @@ -44,46 +95,77 @@ public long capacity() { + scan.count(KIND_CELL) * DriveTuning.CAPACITY_PER_CELL; } - /** How fast it refills. Heat sinks are the whole of the cooling system. */ - public long chargeRate() { + /** + * How much charge this bank can take in one tick — a THROUGHPUT limit, not a supply. Heat sinks + * are what let a buffer swallow a large inflow without cooking; they do not make the energy, and + * a bank with every sink in the world fills at nothing if nothing is feeding it. + */ + public long acceptRate() { ComponentScan.Result scan = scan(); - return DriveTuning.CAPACITOR_BASE_CHARGE_RATE - + scan.count(KIND_SINK) * DriveTuning.CHARGE_RATE_PER_SINK; + return DriveTuning.CAPACITOR_BASE_ACCEPT_RATE + + scan.count(KIND_SINK) * DriveTuning.ACCEPT_RATE_PER_SINK; } - /** The charge at world-clock tick {@code now}. */ - public long chargeAt(long now) { - return CapacitorCharge.at(baseCharge, since, chargeRate(), capacity(), now); + /** What is in the bank right now. */ + public long charge() { + return Math.min(capacity(), Math.max(0L, charge)); } /** - * Ticks until this bank holds {@code needed}, or {@code -1} when it never will because it cannot - * hold that much. This is the cooldown, and it is a consequence of the build rather than a - * number of its own. + * Take up to {@code amount} of charge from whatever is feeding this bank, bounded by the room left + * and by {@link #acceptRate()}. Returns how much was taken. + * + *

    The RULE lives here and the Forge Energy port is three lines of delegation on top of it, so + * "how much a bank will swallow" is a property of the machine rather than of one adapter — and it + * can be asked about without a capability registry standing up around it.

    + * + * @param simulate report what would be taken without taking it */ - public long ticksUntil(long needed, long now) { - return CapacitorCharge.ticksUntil(baseCharge, since, chargeRate(), capacity(), now, needed); + public long acceptCharge(long amount, boolean simulate) { + if (amount <= 0L) { + return 0L; + } + long room = Math.max(0L, capacity() - charge()); + long accepted = Math.min(Math.min(room, acceptRate()), amount); + if (accepted <= 0L) { + return 0L; + } + if (!simulate) { + charge = charge() + accepted; + markDirty(); + } + return accepted; } /** - * Take {@code amount} out of the bank at {@code now}. Returns how much was actually drawn, which - * is all of it or nothing: half a burst does not open half a window. + * Ticks until this bank holds {@code needed} if it is fed at its full accept rate, or + * {@code -1} when it never will because it cannot hold that much. + * + *

    A BEST CASE, and the honest name for it is a forecast: what the bank could do, not what the + * ship will actually deliver. Whether the inflow is there is the power plant's business, and a + * pilot who has under-built his reactors waits longer than this says.

    */ - public long discharge(long amount, long now) { - long available = chargeAt(now); + public long ticksUntilAtFullInflow(long needed) { + return CapacitorCharge.ticksToReach(charge(), capacity(), acceptRate(), needed); + } + + /** + * Take {@code amount} out of the bank. Returns how much was actually drawn, which is all of it or + * nothing: half a burst does not open half a window. + */ + public long discharge(long amount) { + long available = charge(); if (amount <= 0L || available < amount) { return 0L; } - baseCharge = available - amount; - since = now; + charge = available - amount; markDirty(); return amount; } - /** Fill the bank to the brim as of {@code now}. Used by fixtures and by creative-mode charging. */ - public void fill(long now) { - baseCharge = capacity(); - since = now; + /** Fill the bank to the brim. Used by fixtures and by creative-mode charging. */ + public void fill() { + charge = capacity(); markDirty(); } @@ -106,18 +188,32 @@ public String kindAt(BlockPos at) { }, DriveTuning.MAX_CAPACITOR_COMPONENTS); } + @Override + public boolean hasCapability(net.minecraftforge.common.capabilities.Capability capability, + @Nullable EnumFacing facing) { + return capability == CapabilityEnergy.ENERGY || super.hasCapability(capability, facing); + } + + @Override + @Nullable + public T getCapability(net.minecraftforge.common.capabilities.Capability capability, + @Nullable EnumFacing facing) { + if (capability == CapabilityEnergy.ENERGY) { + return CapabilityEnergy.ENERGY.cast(port); + } + return super.getCapability(capability, facing); + } + @Override public NBTTagCompound writeToNBT(NBTTagCompound nbt) { super.writeToNBT(nbt); - nbt.setLong(NBT_BASE_CHARGE, baseCharge); - nbt.setLong(NBT_SINCE, since); + nbt.setLong(NBT_CHARGE, charge); return nbt; } @Override public void readFromNBT(NBTTagCompound nbt) { super.readFromNBT(nbt); - baseCharge = nbt.getLong(NBT_BASE_CHARGE); - since = nbt.getLong(NBT_SINCE); + charge = nbt.getLong(NBT_CHARGE); } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/VSJumpDriveFixtureBoardingE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/VSJumpDriveFixtureBoardingE2ETest.java index 6aee88ee0..cab0293de 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/VSJumpDriveFixtureBoardingE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/VSJumpDriveFixtureBoardingE2ETest.java @@ -250,13 +250,17 @@ public void aJumpCraftAssemblesWholeAndBothItsConsolesAnswerARealKeyPress() thro String cooled = exec("artest drive info 0 " + afcSub[0] + " " + afcSub[1] + " " + afcSub[2]); long cooldown = readLong(cooled, "cooldownTicks"); long burst = readLong(cooled, "burstCost"); + // The cooldown is now burst / the bank's ACCEPT rate — a best case at full inflow — and heat + // sinks are what raise that ceiling. So the shape under test is unchanged: read the implied + // throughput back out and compare it against what a bare controller alone would allow. long observedRate = cooldown > 0L ? burst / cooldown : Long.MAX_VALUE; - assertTrue("the HEAT SINKS must be cooling this ship's bank. A drained bank refilling at " - + observedRate + "/tick (burst " + burst + " over " + cooldown + " ticks) is " - + "what an uncooled controller alone does — the sinks rode into subspace but " - + "the bank is not walking to them. emptied=" + emptied + " info=" + cooled, + assertTrue("the HEAT SINKS must be raising this ship's bank throughput. A drained bank quoted " + + "at " + observedRate + "/tick (burst " + burst + " over " + cooldown + + " ticks) is what an uncooled controller alone allows — the sinks rode into " + + "subspace but the bank is not walking to them. emptied=" + emptied + + " info=" + cooled, cooldown >= 0L && burst > 0L - && observedRate > DriveTuning.CAPACITOR_BASE_CHARGE_RATE * 2L); + && observedRate > DriveTuning.CAPACITOR_BASE_ACCEPT_RATE * 2L); String gate = exec("artest nav gate 0 " + afcSub[0] + " " + afcSub[1] + " " + afcSub[2]); assertTrue("the ship must find its own NAVIGATION COMPUTER from the flight computer. That " diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/CapacitorChargeTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/CapacitorChargeTest.java index 3b9daf6a8..30a63c891 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/CapacitorChargeTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/CapacitorChargeTest.java @@ -3,88 +3,199 @@ import org.junit.Test; import zmaster587.advancedRocketry.hyperdrive.CapacitorCharge; +import zmaster587.advancedRocketry.hyperdrive.DriveTuning; +import zmaster587.advancedRocketry.tile.hyperdrive.TileJumpCapacitor; + +import net.minecraft.nbt.NBTTagCompound; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; /** - * What the capacitor promises: a charge that is computed, never accumulated. + * What the jump bank promises now that it holds real energy. + * + *

    The contract this file used to assert has been RETRACTED, and that is worth stating rather + * than quietly rewriting. It pinned "time away is time charging" and "an unloaded month charges + * exactly like a loaded one" — properties of a capacitor whose level was a closed form of the world + * clock and which therefore stored no energy at all. They were only true because the charge was FREE: + * the biggest single cost in the hyperdrive family, the window burst at twenty times the drive's + * power, was paid for in wall-clock time. An unloaded ship's reactors are not running either, so + * charging through an absence was manufacturing energy a second time, more quietly.

    * - *

    The contracts under test are the ones the rest of the game leans on. Time away is time - * charging, whether or not anything was loaded to notice — that is what lets a ship park in an empty - * cell for a month and come back ready. A bank never overfills, never goes negative, and never gains - * anything from a clock that ran backwards. And the cooldown between jumps is not a timer at all: it - * is however long the same arithmetic takes to reach the next burst, so a bank too small to ever - * hold one says so instead of counting forever.

    + *

    What is pinned instead is that the energy comes from the SHIP: a bank with nothing feeding it + * never fills however long anybody waits, it accepts no faster than its throughput allows, and it + * refuses to be used as a battery by the rest of the vessel. Plus the one thing that was never wrong — + * turning a deficit and a rate into a number of ticks — now labelled as the best case it is.

    */ public class CapacitorChargeTest { - @Test - public void timeAwayIsTimeCharging() { - long atStart = CapacitorCharge.at(0L, 100L, 5L, 1_000_000L, 100L); - long after200Ticks = CapacitorCharge.at(0L, 100L, 5L, 1_000_000L, 300L); + /** + * A capacitor with no world, so its build is just the controller block: capacity + * {@code CAPACITOR_BASE_CAPACITY}, throughput {@code CAPACITOR_BASE_ACCEPT_RATE}. Enough to pin + * every property here, none of which is about the scan. + */ + private static TileJumpCapacitor bareCapacitor() { + return new TileJumpCapacitor(); + } - assertEquals("nothing has elapsed yet", 0L, atStart); - assertTrue("200 ticks of absence must have charged the bank: " + after200Ticks, - after200Ticks > atStart); + /** + * What the ship pushes in. The Forge Energy port itself cannot be exercised here — its + * {@code Capability} handle is injected by Forge and is null outside a loaded game — so the tests + * drive the RULE the port delegates to, which is where the rule belongs. + */ + private static long push(TileJumpCapacitor capacitor, long amount) { + return capacitor.acceptCharge(amount, false); } + // ── the energy is the ship's ────────────────────────────────────────────── + @Test - public void anUnloadedMonthChargesExactlyLikeALoadedOne() { - // The whole point of computing rather than ticking: two capacitors, same build, same elapsed - // time, one of them in a cell nobody visited. They must agree. - long month = 20L * 60L * 60L * 24L * 30L; - long ticked = CapacitorCharge.at(0L, 0L, 1L, Long.MAX_VALUE, month); - long parked = CapacitorCharge.at(0L, 0L, 1L, Long.MAX_VALUE, month); - - assertEquals(ticked, parked); - assertEquals("and the closed form is exactly rate x elapsed", month, ticked); + public void aBankWithNothingFeedingItNeverFills() { + // THE property the old model got wrong. This capacitor is asked about repeatedly and nothing + // ever pushes into it; it must stay empty, because a buffer is not a generator. + TileJumpCapacitor capacitor = bareCapacitor(); + + assertEquals("a fresh bank is empty", 0L, capacitor.charge()); + for (int i = 0; i < 1_000; i++) { + assertEquals("a bank nobody feeds must not gain charge by being asked about it", + 0L, capacitor.charge()); + } + assertEquals("and no elapsed anything fills it either", 0L, capacitor.charge()); } @Test - public void chargeNeverExceedsCapacity() { - long charge = CapacitorCharge.at(0L, 0L, 1_000L, 5_000L, 1_000_000L); + public void whatTheShipPushesInIsWhatTheBankHolds() { + TileJumpCapacitor capacitor = bareCapacitor(); - assertEquals("a full bank is full, however long it waits", 5_000L, charge); + long accepted = push(capacitor, 5L); + assertEquals("the bank takes what it is given, up to its throughput", 5L, accepted); + assertEquals(5L, capacitor.charge()); } @Test - public void aClockThatRanBackwardsGainsNothing() { - // A restored world can hand back a smaller tick count than a tile remembers. That must read - // as "no time has passed", never as a negative charge or a wrapped one. - long charge = CapacitorCharge.at(4_000L, 9_000L, 10L, 10_000L, 500L); + public void aBankAcceptsNoFasterThanItsThroughputAllows() { + // Heat sinks are what raise this. They do not make energy — a bank with every sink in the world + // fills at nothing if nothing is feeding it, which is the previous test. + TileJumpCapacitor capacitor = bareCapacitor(); + + long accepted = push(capacitor, Long.MAX_VALUE); + assertEquals("one tick may not swallow more than the accept rate", + DriveTuning.CAPACITOR_BASE_ACCEPT_RATE, accepted); + assertEquals(DriveTuning.CAPACITOR_BASE_ACCEPT_RATE, capacitor.charge()); + } - assertEquals(4_000L, charge); + @Test + public void aBankNeverOverfills() { + TileJumpCapacitor capacitor = bareCapacitor(); + long capacity = capacitor.capacity(); + + long pushed = 0L; + for (int i = 0; i < 100_000 && capacitor.charge() < capacity; i++) { + pushed += push(capacitor, Long.MAX_VALUE); + } + assertEquals("a full bank is full", capacity, capacitor.charge()); + assertEquals("and it never took more than it can hold", capacity, pushed); + assertEquals("a full bank accepts nothing further", 0L, push(capacitor, 1_000L)); } @Test - public void anAbsenceLongEnoughToOverflowStillJustFills() { - long charge = CapacitorCharge.at(0L, 0L, Long.MAX_VALUE / 2L, 10_000L, Long.MAX_VALUE / 2L); + public void aSimulatedPushChangesNothing() { + TileJumpCapacitor capacitor = bareCapacitor(); - assertEquals("a colossal elapsed time must saturate at capacity, not wrap negative", - 10_000L, charge); + long would = capacitor.acceptCharge(3L, true); + assertEquals("a simulation must report what a real push would take", 3L, would); + assertEquals("...and must not have taken it", 0L, capacitor.charge()); } @Test - public void theCooldownIsHowLongTheNextBurstTakesToArrive() { - long ticks = CapacitorCharge.ticksUntil(0L, 0L, 10L, 10_000L, 0L, 1_000L); + public void theJumpBankIsNOTtheShipsBattery() { + // Only the drive's own burst may take from it. If the rest of the vessel could pull, a jump + // bank would become the ship's general storage and the burst would be paid for out of whatever + // happened to be lying around at the moment — which is the free energy coming back sideways. + // The port cannot be exercised without Forge's capability registry, so what is pinned here is + // the machine's own rule: nothing but the drive's burst removes charge, and the burst goes + // through discharge(). The port's refusal is one line of delegation over this. + TileJumpCapacitor capacitor = bareCapacitor(); + push(capacitor, 10L); + + assertEquals("a partial take must remove nothing", 0L, capacitor.discharge(11L)); + assertEquals("the charge is untouched", 10L, capacitor.charge()); + } + + // ── the burst really leaves the buffer ──────────────────────────────────── - assertEquals("1000 needed at 10 per tick", 100L, ticks); + @Test + public void aBurstTakesAllOfItOrNoneOfIt() { + // Half a burst does not open half a window, so a bank that cannot cover one must not be + // partially drained by the attempt. + TileJumpCapacitor capacitor = bareCapacitor(); + capacitor.fill(); + long full = capacitor.charge(); + assertTrue("the fixture needs a bank with something in it", full > 0L); + + assertEquals("a burst larger than the bank takes nothing", 0L, + capacitor.discharge(full + 1L)); + assertEquals("...and leaves it untouched", full, capacitor.charge()); + + assertEquals("a burst it can cover takes exactly that", full - 1L, + capacitor.discharge(full - 1L)); + assertEquals("and the energy is really gone", 1L, capacitor.charge()); + } + + @Test + public void aStoredChargeIsREADbackOffTheSave() { + // It is real stored energy now, so it has to persist — under the old model only c0 and a tick + // stamp were written and the level was recomputed, which is exactly how an absence created it. + // + // ONLY THE READ HALF is pinned here, and the limit is worth naming rather than hiding: writing + // goes through TileEntity's registry mapping, which does not exist outside a loaded game, so + // this test builds the compound by hand. The write half is exercised by the real save path in + // the server tier but is not ASSERTED anywhere yet, and saying so is better than a green that + // reads as though it were. + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setLong("capCharge", 7L); + + TileJumpCapacitor restored = bareCapacitor(); + restored.readFromNBT(nbt); + assertEquals("a reloaded bank holds what it held", 7L, restored.charge()); + } + + // ── the cooldown forecast, now a best case ─────────────────────────────── + + @Test + public void theForecastIsADeficitOverARate() { + assertEquals("already there", 0L, CapacitorCharge.ticksToReach(500L, 1_000L, 10L, 500L)); + assertEquals("400 short at 10 a tick", 40L, + CapacitorCharge.ticksToReach(100L, 1_000L, 10L, 500L)); + assertEquals("a partial tick still costs a whole one", 41L, + CapacitorCharge.ticksToReach(99L, 1_000L, 10L, 500L)); } @Test - public void aBankThatAlreadyHoldsTheBurstHasNoCooldown() { - assertEquals(0L, CapacitorCharge.ticksUntil(5_000L, 0L, 10L, 10_000L, 0L, 1_000L)); + public void aBankTooSmallToEverHoldABurstSaysSoInsteadOfCountingForever() { + assertEquals(-1L, CapacitorCharge.ticksToReach(0L, 1_000L, 10L, 5_000L)); } @Test - public void aBankTooSmallForTheBurstSaysSoInsteadOfCountingForever() { - assertEquals("a build that can never open the window must be reported, not waited on", - -1L, CapacitorCharge.ticksUntil(0L, 0L, 10L, 500L, 0L, 1_000L)); + public void aBankWithNoInflowNeverGetsThere() { + // The forecast's own statement of the property the first test pins on the tile: a rate of zero + // is not "a very long time", it is never. + assertEquals(-1L, CapacitorCharge.ticksToReach(0L, 10_000L, 0L, 5_000L)); } @Test - public void aBankThatNeverRechargesSaysSoToo() { - assertEquals(-1L, CapacitorCharge.ticksUntil(0L, 0L, 0L, 10_000L, 0L, 1_000L)); + public void theForecastIsTheBANKSbestCaseAndTheTileSaysSo() { + // Named for what it is. The rate is the bank's own accept ceiling, so a ship whose reactors + // deliver less waits longer — and nothing here may present that number as a promise. + TileJumpCapacitor capacitor = bareCapacitor(); + long needed = capacitor.capacity(); + long forecast = capacitor.ticksUntilAtFullInflow(needed); + + assertEquals("an empty bank at its full accept rate", + (needed + DriveTuning.CAPACITOR_BASE_ACCEPT_RATE - 1L) + / DriveTuning.CAPACITOR_BASE_ACCEPT_RATE, + forecast); + assertEquals("a burst bigger than the bank is never reachable", -1L, + capacitor.ticksUntilAtFullInflow(needed + 1L)); } } From e5b6ef3117d360761786c06366b6c0c31e6fdb37 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 20:02:37 +0300 Subject: [PATCH 34/42] test: the jump bank is filled by the ship, and it is measured - a drained bank stays at zero through 100 real server ticks - artest drive push feeds it through the real energy capability - one push is bounded by throughput, not by capacity - a charge survives a real chunk save, drop and reload --- .../command/test/TestProbeCommand.java | 25 ++++++- .../test/server/HyperdriveE2ETest.java | 73 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 8528dbc64..ce1f5573b 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -2689,7 +2689,7 @@ private zmaster587.advancedRocketry.tile.multiblock.TileObservatory observatoryA private void handleDrive(MinecraftServer server, ICommandSender sender, String[] args) { if (args.length < 5) { - send(sender, "{\"error\":\"usage: drive build|info|charge|arm|press|hull ...\"}"); + send(sender, "{\"error\":\"usage: drive build|info|charge|push|arm|press|hull ...\"}"); return; } String verb = args[0]; @@ -2746,6 +2746,29 @@ private void handleDrive(MinecraftServer server, ICommandSender sender, String[] send(sender, "{\"ok\":true,\"charge\":" + drive.capacitorCharge() + "}"); return; } + if ("push".equalsIgnoreCase(verb)) { + // Energy pushed in THROUGH THE FORGE ENERGY CAPABILITY, which is what an adjacent reactor, + // solar array or cable does. Deliberately not `fill()`: that seam sets the level directly + // and would leave a test unable to tell a wired bank from one that manufactures its own + // charge — which is the exact defect this verb exists to be able to observe. + long amount = args.length > 5 ? parseLongOr(args[5], 0L) : 0L; + long accepted = 0L; + int ports = 0; + for (zmaster587.advancedRocketry.tile.hyperdrive.TileJumpCapacitor capacitor + : drive.capacitors()) { + net.minecraftforge.energy.IEnergyStorage port = capacitor.getCapability( + net.minecraftforge.energy.CapabilityEnergy.ENERGY, null); + if (port == null) { + continue; + } + ports++; + accepted += port.receiveEnergy( + (int) Math.min(Integer.MAX_VALUE, Math.max(0L, amount)), false); + } + send(sender, "{\"ok\":true,\"ports\":" + ports + ",\"accepted\":" + accepted + + ",\"charge\":" + drive.capacitorCharge() + "}"); + return; + } if ("arm".equalsIgnoreCase(verb)) { zmaster587.advancedRocketry.navigation.ShipNavigation nav = new zmaster587.advancedRocketry.navigation.ShipNavigation(world, afc, shipId); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/HyperdriveE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/HyperdriveE2ETest.java index b43726114..353d29c59 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/HyperdriveE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/HyperdriveE2ETest.java @@ -286,4 +286,77 @@ public void dampenersAreFoundAndReportPowered() throws Exception { assertEquals("and a dampener with power in its buffer is one that will protect somebody", 3L, field(info, "poweredDampeners")); } + + // ─── The bank is filled by the SHIP, not by the clock ────────────────────── + + /** Its own site: this family drains, feeds and unloads a bank, and must disturb nobody else. */ + private static final String SHIP_E = "2840 82 2840"; + + @Test + public void aFRESHBANKSTAYSEMPTYWHILETIMEPASSES() throws Exception { + // THE property the old model got wrong, asked of a real world with a real clock — which is the + // strongest form of the question, because the defect WAS the clock. The bank used to be a closed + // form of elapsed ticks, so the biggest cost in the family (the window burst, twenty times the + // drive's power) was paid for by waiting. A buffer nobody feeds must stay at nothing. + buildDrive(SHIP_E, 4, 8, 4, 0, 0); + exec("artest drive charge 0 " + SHIP_E + " empty"); + + long before = field(exec("artest drive info 0 " + SHIP_E), "charge"); + assertEquals("a drained bank starts empty", 0L, before); + + zmaster587.advancedRocketry.test.ServerTicks.await(client(), 0, 100); + + String after = exec("artest drive info 0 " + SHIP_E); + assertEquals("100 ticks of a running server must not have put a single unit into a bank that" + + " nothing is feeding: " + after, 0L, field(after, "charge")); + assertTrue("and it must still WANT charge, or this proves nothing", + field(after, "burstCost") > 0L); + } + + @Test + public void whatTheSHIPPUSHESINthroughItsGridIsWhatTheBankHolds() throws Exception { + // The positive half of the same wiring, and it goes through the real Forge Energy capability — + // the same one an adjacent reactor, array or cable pushes into — rather than through the + // fixture seam that sets the level directly. + buildDrive(SHIP_E, 4, 8, 4, 0, 0); + exec("artest drive charge 0 " + SHIP_E + " empty"); + + String pushed = exec("artest drive push 0 " + SHIP_E + " 1000000000"); + assertTrue("the bank must expose an energy port for the ship to push into: " + pushed, + field(pushed, "ports") > 0L); + long accepted = field(pushed, "accepted"); + assertTrue("and it must have taken some of it: " + pushed, accepted > 0L); + assertEquals("what it took is what it holds", accepted, + field(exec("artest drive info 0 " + SHIP_E), "charge")); + + // One push is one tick's worth: the accept rate is a THROUGHPUT ceiling, so a billion offered + // at once does not fill a bank that a hundred pushes would. + long capacity = field(exec("artest drive info 0 " + SHIP_E), "capacity"); + assertTrue("a single tick of inflow must not fill the whole bank (" + accepted + " of " + + capacity + ")", capacity <= 0L || accepted < capacity); + + String again = exec("artest drive push 0 " + SHIP_E + " 1000000000"); + assertTrue("a second push must add more", field(again, "charge") > accepted); + } + + @Test + public void aBanksChargeSurvivesAREALunloadAndReload() throws Exception { + // The write half of the persistence contract, which only a real save can exercise: a + // force-loaded chunk never leaves memory, so a test against one proves the object was not + // collected rather than that its NBT round-trips. `chunk cycle` saves, drops and reads back. + buildDrive(SHIP_E, 4, 8, 4, 0, 0); + exec("artest drive charge 0 " + SHIP_E + " full"); + long before = field(exec("artest drive info 0 " + SHIP_E), "charge"); + assertTrue("the fixture needs a bank with something in it", before > 0L); + + int cx = 2840 >> 4; + int cz = 2840 >> 4; + String cycled = exec("artest chunk cycle 0 " + cx + " " + cz); + assertTrue("the chunk must really have left memory, or nothing was read back from disk: " + + cycled, cycled.contains("\"dropped\":true")); + + String after = exec("artest drive info 0 " + SHIP_E); + assertEquals("a bank that came back from disk holds what it held: " + after, before, + field(after, "charge")); + } } From 11084ff8141b536e6d8f60cccade4f595c5b8c8a Mon Sep 17 00:00:00 2001 From: StannisMod Date: Mon, 17 Aug 2026 21:34:43 +0300 Subject: [PATCH 35/42] feat: the void holds what the galaxies threw out of themselves - add an ejecta halo outside every galaxy's declared radius - draw the star lattice a second time for unbound systems - add ROGUE_PLANET and a starless branch in PlanetDerivation - rename StarSystem to PlanetarySystem with an optional star - seat self-bound clusters outside a galaxy with their own field --- .../universe/ClusterField.java | 78 +++- .../universe/ClusteredGalaxyGenerator.java | 318 ++++++++++++---- .../universe/EmptyGalaxyGenerator.java | 4 +- .../advancedRocketry/universe/Galaxy.java | 54 ++- .../universe/GalaxyField.java | 75 ++++ .../universe/GalaxyGenConfig.java | 83 +++- .../universe/IGalaxyGenerator.java | 4 +- .../universe/PlanetDerivation.java | 89 +++++ .../universe/PlanetarySystem.java | 107 ++++++ .../advancedRocketry/universe/StarSystem.java | 56 --- .../advancedRocketry/universe/SystemBody.java | 3 +- .../universe/SystemBodyKind.java | 32 +- .../universe/TelescopeScan.java | 12 +- .../universe/UniverseRegistry.java | 94 ++++- .../test/integration/SystemContentTest.java | 11 +- .../unit/ClusteredGalaxyGeneratorTest.java | 101 +++-- .../test/unit/DriveLadderTest.java | 4 +- .../test/unit/GalaxyFieldTest.java | 20 +- .../unit/InterstellarLegDistanceTest.java | 6 +- .../test/unit/NebulaConcealmentTest.java | 6 +- .../test/unit/NebulaTest.java | 2 +- .../test/unit/SkyNebulaeProducerTest.java | 6 +- .../test/unit/StarClusterTest.java | 2 +- .../test/unit/SystemRetinueTest.java | 30 +- .../test/unit/TelescopeRegionScanTest.java | 4 +- .../test/unit/UniverseRegistryTest.java | 48 +-- .../test/unit/VoidContentTest.java | 357 ++++++++++++++++++ 27 files changed, 1335 insertions(+), 271 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/PlanetarySystem.java delete mode 100644 src/main/java/zmaster587/advancedRocketry/universe/StarSystem.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/VoidContentTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java index 70733e3fc..65ce6ec4e 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java @@ -29,30 +29,36 @@ public final class ClusterField { private static final long SALT_NUCLEUS_RADIUS = 0x207L; private final GalaxyGenConfig config; + private final GalaxyField galaxies; private final long spacingSuperCells; - private final long totalClusterWeight; - public ClusterField(GalaxyGenConfig config) { + /** + * @param galaxies the tier above — what a cluster cell's occupancy is scaled by. A cluster inside a + * galaxy is scaled by that galaxy's profile; one out in the void is scaled by the + * ejecta halo, which is how a globular can be intergalactic without a second rule + */ + public ClusterField(GalaxyGenConfig config, GalaxyField galaxies) { this.config = (config == null) ? GalaxyGenConfig.defaults() : config; + this.galaxies = (galaxies == null) ? new GalaxyField(this.config) : galaxies; this.spacingSuperCells = Math.max(1L, superCellsForLightYears(GalaxyGenConfig.CLUSTER_SPACING_LY, this.config.minSpacing)); - long w = 0L; - for (GalaxyGenConfig.ClusterType t : this.config.clusterTypes) { - w += t.weight; - } - this.totalClusterWeight = Math.max(1L, w); } /** * The cluster this coarse super-cell belongs to, or empty when it is ordinary field. * - *

    {@code galaxy} is the galaxy that owns the super-cell; a cluster outside a galaxy is not a - * thing this generator makes, because there would be no stars to gather.

    + *

    {@code galaxy} is the galaxy the super-cell is INSIDE, and it may be {@code null}: a cluster + * out in the intergalactic void is a real object — a globular thrown clear of the galaxy it + * formed around, still bound to itself. It used to be refused by construction here, on the + * reasoning that there would be no stars out there to gather; what that missed is that a cluster + * does not gather the field, it BRINGS its own. Its occupancy is scaled by the material at its own + * cell, which out there is the ejecta halo — so intergalactic globulars thin out with the void and + * cluster near the galaxies that threw them, on the same one function that places everything else.

    + * + *

    A galaxy-less cluster has no NUCLEUS, and that is not a special case either: a nucleus is the + * cluster at a galaxy's own centre, and there is no galaxy here to have one.

    */ public Optional clusterAt(long seed, Galaxy galaxy, long supX, long supY, long supZ) { - if (galaxy == null) { - return Optional.empty(); - } Optional nucleus = nucleusOf(seed, galaxy); if (nucleus.isPresent() && nucleus.get().containsSuperCell(supX, supY, supZ)) { return nucleus; @@ -114,8 +120,9 @@ private static int nucleusSubdivisionFor(Galaxy galaxy) { /** * The cluster seated in cluster cell {@code (cx, cy, cz)}, or empty. * - *

    Occupancy is scaled by the galaxy's own density profile at the cell, so clusters live where - * stars live and stop where the galaxy stops — one function, not a second rule.

    + *

    Occupancy is scaled by the material at the cell, so clusters live where material lives: the + * galaxy's own profile inside one, and the ejecta halo outside — one function, not a second rule. + * {@code galaxy} may be {@code null} for a cluster cell out in the void.

    */ public Optional clusterAtIndex(long seed, Galaxy galaxy, long cx, long cy, long cz) { long s = config.minSpacing; @@ -124,7 +131,11 @@ public Optional clusterAtIndex(long seed, Galaxy galaxy, long cx, l long sectorX = (cx * spacingSuperCells + centreSuper) * s; long sectorY = (cy * spacingSuperCells + centreSuper) * s; long sectorZ = (cz * spacingSuperCells + centreSuper) * s; - double profile = galaxy.densityAtSector(sectorX, sectorY, sectorZ); + // Inside a galaxy the caller has already resolved which one, so read it directly rather than + // walking the cube again; out in the void there is nothing resolved and the halo is the answer. + double profile = galaxy != null + ? galaxy.densityAtSector(sectorX, sectorY, sectorZ) + : galaxies.materialAtSector(seed, sectorX, sectorY, sectorZ).total(); if (!(profile > 0d)) { return Optional.empty(); } @@ -133,7 +144,16 @@ public Optional clusterAtIndex(long seed, Galaxy galaxy, long cx, l return Optional.empty(); } - GalaxyGenConfig.ClusterType type = pickType(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_TYPE)); + // Out in the void only a SELF-BOUND cluster is seated: an open cluster disperses in a few + // hundred million years and a molecular cloud never was bound, so neither survives the + // crossing it would have had to make to be out here. Expressed as a constraint on the DRAW + // rather than a clamp on its result, which is the same shape the satellite-size and + // authored-galaxy floors use. + GalaxyGenConfig.ClusterType type = pickType(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_TYPE), + galaxy == null); + if (type == null) { + return Optional.empty(); // no type qualifies — an honest answer, not an error + } double radiusFraction = CellHash.norm(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_RADIUS)); double radiusLy = type.minRadiusLy + radiusFraction * (type.maxRadiusLy - type.minRadiusLy); long radius = superCellsForLightYears(radiusLy, config.minSpacing); @@ -162,16 +182,36 @@ private static long superCellsForLightYears(double lightYears, long superCellEdg return Math.max(1L, cells / Math.max(1L, superCellEdgeCells)); } - private GalaxyGenConfig.ClusterType pickType(long h) { - long r = Math.floorMod(h, totalClusterWeight); + /** + * A weighted draw over the cluster table, or {@code null} when nothing qualifies. + * + * @param selfBoundOnly restrict to the types that survive outside a galaxy — the weights of the + * rest are then not merely skipped but EXCLUDED from the total, so the + * qualifying types keep their relative abundance instead of the draw falling + * through to whichever one happens to be last + */ + private GalaxyGenConfig.ClusterType pickType(long h, boolean selfBoundOnly) { + long total = 0L; + for (GalaxyGenConfig.ClusterType t : config.clusterTypes) { + if (!selfBoundOnly || t.selfBound) { + total += t.weight; + } + } + if (total <= 0L) { + return null; + } + long r = Math.floorMod(h, total); GalaxyGenConfig.ClusterType last = null; for (GalaxyGenConfig.ClusterType t : config.clusterTypes) { + if (selfBoundOnly && !t.selfBound) { + continue; + } last = t; if (r < t.weight) { return t; } r -= t.weight; } - return last; // config.clusterTypes is never empty + return last; } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index f221da1b4..c4871e2c6 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -122,6 +122,16 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { private static final long SALT_MOONANG = 0x17L; private static final long SALT_MOONRAD = 0x18L; + // The UNBOUND draw: a second, independent roll on the same lattice cell, so a cube the star draw + // passed over may still hold something. Its own salts, so the two rolls cannot correlate — a shared + // stream would make "no star here" and "a rogue here" the same coin toss read twice. + private static final long SALT_ROGUE_OCC = 0x19L; + private static final long SALT_ROGUE_TYPE = 0x1AL; + private static final long SALT_ROGUE_ID = 0x1BL; + private static final long SALT_ROGUE_MOONCOUNT = 0x1CL; + private static final long SALT_ROGUE_MOONANG = 0x1DL; + private static final long SALT_ROGUE_MOONRAD = 0x1EL; + // ─── The retinue: how many bodies a system has, and where they sit ───────── // Every number here is a balance knob. What is NOT a knob is the shape: a long tail, a mandatory // outer belt, and moons on the bodies big enough to hold them. @@ -182,17 +192,25 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { private final ClusterField clusters; private final NebulaField nebulae; private final long totalStarWeight; + private final List rogueTypes; + private final long totalRogueWeight; public ClusteredGalaxyGenerator(GalaxyGenConfig config) { this.config = (config == null) ? GalaxyGenConfig.defaults() : config; this.galaxies = new GalaxyField(this.config); - this.clusters = new ClusterField(this.config); + this.clusters = new ClusterField(this.config, this.galaxies); this.nebulae = new NebulaField(this.config, this.clusters); long w = 0L; // accumulate in long so a few near-Integer.MAX weights cannot overflow the sum for (GalaxyGenConfig.StarType t : this.config.starTypes) { w += t.weight; } this.totalStarWeight = Math.max(1L, w); + this.rogueTypes = GalaxyGenConfig.defaultRogueTypes(); + long rw = 0L; + for (GalaxyGenConfig.RogueType t : this.rogueTypes) { + rw += t.weight; + } + this.totalRogueWeight = Math.max(1L, rw); } public GalaxyGenConfig config() { @@ -219,7 +237,7 @@ public NebulaField nebulae() { } @Override - public Optional systemAt(long seed, GalacticCoord coord) { + public Optional systemAt(long seed, GalacticCoord coord) { Optional g = systemForLattice(seed, latticeAt(seed, coord.sectorX(), coord.sectorY(), coord.sectorZ())); if (g.isPresent() && g.get().cell.sameCell(coord)) { @@ -241,7 +259,7 @@ public Optional systemAt(long seed, GalacticCoord coord) { * outcome worse than a slow scan.

    */ @Override - public Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { + public Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { long s = config.minSpacing; long loX = Math.min(min.sectorX(), max.sectorX()); long hiX = Math.max(min.sectorX(), max.sectorX()); @@ -250,12 +268,13 @@ public Map systemsInRegion(long seed, GalacticCoord m long loZ = Math.min(min.sectorZ(), max.sectorZ()); long hiZ = Math.max(min.sectorZ(), max.sectorZ()); - Map out = new HashMap<>(); + Map out = new HashMap<>(); boolean capped = false; for (long supX = Math.floorDiv(loX, s); supX <= Math.floorDiv(hiX, s) && !capped; supX++) { for (long supY = Math.floorDiv(loY, s); supY <= Math.floorDiv(hiY, s) && !capped; supY++) { for (long supZ = Math.floorDiv(loZ, s); supZ <= Math.floorDiv(hiZ, s) && !capped; supZ++) { - int k = subdivisionAt(seed, supX, supY, supZ); + LocalField local = localFieldAt(seed, supX, supY, supZ); + int k = local.subdivision; // Only the sub-cells the query box actually reaches. A system seated in a // sub-cell is placed INSIDE it, so this is exactly the same answer as walking all // k³ and filtering — and it is the difference between a bounded query and a @@ -270,7 +289,7 @@ public Map systemsInRegion(long seed, GalacticCoord m for (long j = jLo; j <= jHi && !capped; j++) { for (long m = mLo; m <= mHi && !capped; m++) { Optional g = systemForLattice(seed, - Lattice.of(supX, supY, supZ, i, j, m, k, s)); + Lattice.of(supX, supY, supZ, i, j, m, k, s, local.ownField)); if (!g.isPresent()) { continue; } @@ -303,16 +322,22 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { return Collections.emptyList(); } GalacticCoord cell = anchorOpt.get(); - Optional sys = systemAt(seed, cell); + Optional sys = systemAt(seed, cell); if (!sys.isPresent()) { return Collections.emptyList(); } - int starId = sys.get().starId(); - StellarBody star = sys.get().star(); + int systemId = sys.get().systemId(); + if (!sys.get().star().isPresent()) { + // A system whose primary is not a star: no companions, no zone, no orbits — the whole + // second half of the retinue law is about distances FROM a star. What it can still have is + // moons, so that is what it gets. + return rogueBodiesFor(seed, cell, systemId); + } + StellarBody star = sys.get().star().get(); List bodies = new ArrayList<>(); // The star sits at the anchor cell's centre. // A star does not move inside its own system: its frame IS the system's anchor. - bodies.add(SystemBody.fixedAt(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, starId)); + bodies.add(SystemBody.fixedAt(cell, SystemBodyKind.STAR, Constants.INVALID_PLANET, systemId)); // A body sits where its ORBIT puts it — one law, one constant, the same one an authored system // uses. What the neighbourhood decides is not how far a body goes but how many bodies there is @@ -357,11 +382,56 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { .withRadius(AstronomicalBodyHelper.starRadiusEarths(companion))); } - appendRetinue(bodies, seed, cell, star, starId, lattice, taken, outerBound, + appendRetinue(bodies, seed, cell, star, systemId, lattice, taken, outerBound, retinueSize(seed, cell)); return bodies; } + /** + * The bodies of a system anchored on a STARLESS world — the rogue itself, and whatever it kept. + * + *

    It is a short list on purpose. There is no belt, because an unbound world carries no disc: a + * belt is material that never accreted in a star's own gravity well, and this world left that well + * behind. There is no orbit and no zone, so nothing here is placed by distance from anything.

    + * + *

    Moons it may keep, and few. Whatever unbound a planet from its star pulled far harder + * on the loosely-held satellites than on the tight ones, so a rogue arrives out here with the + * inner few and nothing else — the same ceiling a rocky world has, applied whatever its bulk, + * rather than the ceiling its mass would otherwise buy it.

    + */ + private static List rogueBodiesFor(long seed, GalacticCoord cell, int systemId) { + List bodies = new ArrayList<>(); + BodyProfile profile = PlanetDerivation.deriveRogue(seed, cell, 0); + // It does not move inside its own system: it IS the system, so its frame is the anchor's. + bodies.add(SystemBody.fixedAt(cell, SystemBodyKind.ROGUE_PLANET, Constants.INVALID_PLANET, + systemId).withRadius(profile.radiusEarths())); + + double u = CellHash.norm(CellHash.ofCell(seed, cell, SALT_ROGUE_MOONCOUNT)); + int moons = (int) (Math.pow(u, MOON_COUNT_BIAS) * (MAX_MOONS_ROCKY + 1)); + if (moons > MAX_MOONS_ROCKY) { + moons = MAX_MOONS_ROCKY; + } + CellFrame frame = CellFrame.staticAt(cell); + for (int j = 1; j <= moons; j++) { + int moonOrbit = MOON_MIN_ORBIT + (int) (CellHash.norm( + CellHash.ofBody(seed, cell, j, SALT_ROGUE_MOONRAD)) * MOON_ORBIT_SPAN); + double theta = CellHash.norm(CellHash.ofBody(seed, cell, j, SALT_ROGUE_MOONANG)) + * 2d * Math.PI; + double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY + * AstronomicalBodyHelper.getMoonOrbitalPeriod(moonOrbit, + (float) Math.max(0.05d, profile.massEarths())); + BodyEphemeris law = BodyEphemeris.orbit(moonOrbit, theta, 0d, false, periodTicks, + SystemContent.MOON_UNIT_BLOCKS); + // A moon of a rogue is starless too, so it is derived the same way its parent was, one + // variant along — never through the star-lit law with a star that is not there. + BodyProfile moonProfile = PlanetDerivation.deriveRogue(seed, cell, j); + bodies.add(new SystemBody(cell, frame, law, SystemBodyKind.MOON, + Constants.INVALID_PLANET, systemId, SystemBody.ORBIT_UNKNOWN) + .withRadius(moonProfile.radiusEarths())); + } + return bodies; + } + /** * Append a system's RETINUE — its worlds, their moons and its belts — to {@code bodies}. * @@ -625,6 +695,12 @@ private void addMoons(List bodies, long seed, GalacticCoord anchor, */ public BodyProfile profileOf(long seed, GalacticCoord anchor, SystemBody body, StellarBody star, int variant) { + if (star == null) { + // Nothing lights this system, so nothing about the body follows from a distance: it is the + // starless derivation or it is a body whose physics would be read off a star that is not + // there. A moon of a rogue takes the same branch, which is right — it is starless too. + return PlanetDerivation.deriveRogue(seed, body.name(), variant); + } return PlanetDerivation.derive(seed, anchor.cellCentre(), body.name(), variant, star, body.kind() == SystemBodyKind.MOON, body.orbitalDistance()); } @@ -735,15 +811,26 @@ private Optional systemForLattice(long seed, Lattice lattice) { // the probability a cube is occupied cannot depend on where its seat would have landed. And // evaluated at t = 0 and never again: a time-dependent occupancy would pop systems in and out // of existence. Systems drift afterwards at their galaxy's own omega(r), which is the shear. - double profile = galaxyProfileAt(seed, lattice.lowX + lattice.edgeX / 2L, - lattice.lowY + lattice.edgeY / 2L, lattice.lowZ + lattice.edgeZ / 2L); - if (!(profile > 0d)) { - return Optional.empty(); // intergalactic void, or past this galaxy's edge - } + GalaxyField.Material material = galaxies.materialAtSector(seed, + lattice.lowX + lattice.edgeX / 2L, lattice.lowY + lattice.edgeY / 2L, + lattice.lowZ + lattice.edgeZ / 2L); + // A cluster out in the void supplies its own field, because k³ times the halo is still nothing + // and an intergalactic globular has to be a globular. Inside a galaxy ownField is zero and the + // profile speaks, so this is the same number it always was everywhere anything already exists. + double bound = Math.max(material.bound, lattice.ownField); // Keyed by the cell's LOW CORNER, which is globally unique whatever lattice it belongs to — // a coarse index would collide with a fine one wherever a cluster refines the field. - if (CellHash.norm(lattice.hash(seed, SALT_OCC)) >= Math.min(1d, config.density * profile)) { - return Optional.empty(); + boolean star = bound > 0d + && CellHash.norm(lattice.hash(seed, SALT_OCC)) < Math.min(1d, config.density * bound); + if (!star) { + // THE SECOND DRAW, on the cube the first one passed over. Stars need a galaxy to form in; + // an unbound world does not, so out in the void this is the only roll there is, and inside + // a galaxy it is what makes free-floating worlds as numerous as the sky says they are. + // + // It reads material.total(), which is the bound profile inside a galaxy and the ejecta halo + // outside it — so the void's population is what the galaxies have thrown out, on one + // continuous function, rather than a second rule with a density of its own. + return rogueForLattice(seed, lattice, Math.max(material.total(), lattice.ownField)); } // Seat the anchor anywhere in its cube except a declared margin at the faces. That margin is // the system's own CLEAR SPACE, not a fraction of the cube: it is what guarantees two stars @@ -758,11 +845,86 @@ private Optional systemForLattice(long seed, Lattice lattice) { // It is read off the LOCAL edge, so inside a cluster the floor shrinks with the lattice: stars // in a globular core really do stand closer than a wide binary, and a system there loses outer // bodies by the same rule that has always applied. - GalacticCoord cell = GalacticCoord.ofSectorLocal( + return Optional.of(new Generated(seatIn(seed, lattice), fabricate(seed, lattice))); + } + + /** + * What an UNBOUND seat holds, or empty when this cube holds nothing at all. + * + *

    A weighted draw over {@link GalaxyGenConfig#defaultRogueTypes()}, so relative abundance lives + * in a table exactly as it does for star types, galaxy types and cluster types. Two outcomes + * today: a starless world, which is what the void is mostly made of, and a whole STAR SYSTEM that + * was thrown out of its galaxy — rare enough that meeting one out here is an event.

    + * + *

    A rogue star is fabricated by {@link #fabricate}, unchanged, and it is not marked as anything: + * rogue-ness is a statement about WHERE a star stands and not about what it is, so a system out in + * the void is an ordinary system with an ordinary retinue, and the only thing that makes it a find + * is its address.

    + * + * @param profile the material at this cell — the galaxy's own where there is one, its ejecta where + * there is not + */ + private Optional rogueForLattice(long seed, Lattice lattice, double profile) { + if (!(profile > 0d)) { + return Optional.empty(); // a galaxy cell with no galaxy in it: the deepest void, and empty + } + double occupancy = Math.min(1d, config.density * GalaxyGenConfig.ROGUE_ABUNDANCE * profile); + if (CellHash.norm(lattice.hash(seed, SALT_ROGUE_OCC)) >= occupancy) { + return Optional.empty(); + } + GalaxyGenConfig.RogueType type = pickRogueType(lattice.hash(seed, SALT_ROGUE_TYPE)); + if (type.primaryKind == SystemBodyKind.STAR) { + return Optional.of(new Generated(seatIn(seed, lattice), fabricate(seed, lattice))); + } + return Optional.of(new Generated(seatIn(seed, lattice), fabricateRogue(seed, lattice))); + } + + /** + * Where this lattice cell's system sits: anywhere in its cube except a declared margin at the + * faces. + * + *

    That margin is the system's own CLEAR SPACE, not a fraction of the cube: it is what guarantees + * two systems never stand closer than the separation floor, and what keeps one system's named + * bodies from reaching into the next cube, so member-cell attribution stays exact.

    + * + *

    It used to be the middle quarter per axis, which confined the seat to 1.6 % of the cube's + * volume — a lattice of tight clumps with guaranteed-empty walls between them, visible in any + * rendered star field. The margin now costs a couple of percent per face instead, because it is + * sized by what a system actually needs rather than by the distance to the next star.

    + * + *

    It is read off the LOCAL edge, so inside a cluster the floor shrinks with the lattice: stars + * in a globular core really do stand closer than a wide binary, and a system there loses outer + * bodies by the same rule that has always applied.

    + */ + private static GalacticCoord seatIn(long seed, Lattice lattice) { + return GalacticCoord.ofSectorLocal( lattice.lowX + seatOffset(seed, lattice, SALT_OX, lattice.edgeX), lattice.lowY + seatOffset(seed, lattice, SALT_OY, lattice.edgeY), lattice.lowZ + seatOffset(seed, lattice, SALT_OZ, lattice.edgeZ), 0L, 0L, 0L); - return Optional.of(new Generated(cell, fabricate(seed, lattice))); + } + + /** + * A system anchored on a starless world. Its id comes from the same synthetic negative range a + * procedural star's does, through a stream of its own — the id space names systems and does not + * care what kind of thing stands at one. + */ + private static PlanetarySystem fabricateRogue(long seed, Lattice lattice) { + int id = syntheticId(seed, lattice.lowX, lattice.lowY, lattice.lowZ, SALT_ROGUE_ID); + return PlanetarySystem.ofRogue(id, + "PGR-" + lattice.lowX + "." + lattice.lowY + "." + lattice.lowZ); // rogue + } + + private GalaxyGenConfig.RogueType pickRogueType(long h) { + long r = Math.floorMod(h, totalRogueWeight); + GalaxyGenConfig.RogueType last = null; + for (GalaxyGenConfig.RogueType t : rogueTypes) { + last = t; + if (r < t.weight) { + return t; + } + r -= t.weight; + } + return last; // the table is never empty } /** Where the seat sits on one axis of its lattice cell, clear of the faces by the local margin. */ @@ -788,17 +950,23 @@ private static final class Lattice { final long edgeY; final long edgeZ; - private Lattice(long lowX, long lowY, long lowZ, long edgeX, long edgeY, long edgeZ) { + /** See {@link LocalField#ownField} — what a cluster out in the void brings with it. */ + final double ownField; + + private Lattice(long lowX, long lowY, long lowZ, long edgeX, long edgeY, long edgeZ, + double ownField) { this.lowX = lowX; this.lowY = lowY; this.lowZ = lowZ; this.edgeX = edgeX; this.edgeY = edgeY; this.edgeZ = edgeZ; + this.ownField = ownField; } /** Sub-cell {@code (i, j, m)} of coarse super-cell {@code (supX, supY, supZ)}, at {@code k}. */ - static Lattice of(long supX, long supY, long supZ, long i, long j, long m, int k, long s) { + static Lattice of(long supX, long supY, long supZ, long i, long j, long m, int k, long s, + double ownField) { long baseX = supX * s; long baseY = supY * s; long baseZ = supZ * s; @@ -808,7 +976,7 @@ static Lattice of(long supX, long supY, long supZ, long i, long j, long m, int k return new Lattice(baseX + loI, baseY + loJ, baseZ + loM, Math.max(1L, Math.floorDiv((i + 1L) * s, (long) k) - loI), Math.max(1L, Math.floorDiv((j + 1L) * s, (long) k) - loJ), - Math.max(1L, Math.floorDiv((m + 1L) * s, (long) k) - loM)); + Math.max(1L, Math.floorDiv((m + 1L) * s, (long) k) - loM), ownField); } /** Its draw for one field, keyed by the low corner — globally unique at any subdivision. */ @@ -837,31 +1005,63 @@ long minEdge() { } /** - * How finely the lattice is divided at this coarse super-cell: {@code 1} in the ordinary field, - * and the cluster's {@code k} where one covers it. + * What the star lattice looks like at one coarse super-cell: how finely it is divided, and what + * field it is divided AGAINST. * - *

    Membership is a property of the COARSE cell, which is what keeps this an O(1) question with - * one answer — and what makes the fine lattice tile the coarse cells it replaces exactly.

    + *

    Membership of a cluster is a property of the COARSE cell, which is what keeps this an O(1) + * question with one answer — and what makes the fine lattice tile the coarse cells it replaces + * exactly.

    */ - private int subdivisionAt(long seed, long supX, long supY, long supZ) { + private static final class LocalField { + + static final LocalField PLAIN = new LocalField(1, 0d); + + /** {@code 1} in the ordinary field, and the covering cluster's {@code k} where there is one. */ + final int subdivision; + /** + * The field a cluster BRINGS with it, or zero where the surrounding profile already speaks. + * + *

    A cluster's density is expressed as a contrast — {@code k³} times whatever is around it — + * and inside a galaxy that is exactly right, because what is around it is the real solar + * neighbourhood. Out in the void it is a contrast against nearly nothing, and {@code k³} times + * nearly nothing is still nothing: an intergalactic globular would be named, addressable and + * empty. A globular does not gather the field it sits in; it arrived carrying its own.

    + */ + final double ownField; + + LocalField(int subdivision, double ownField) { + this.subdivision = subdivision; + this.ownField = ownField; + } + } + + /** + * The field a cluster outside every galaxy supplies, on {@link Galaxy#densityAt}'s scale. + * + *

    One, and derived rather than picked: that profile is normalised at the sun-like galactic + * radius, so {@code 1} IS the density of an ordinary stellar neighbourhood. A globular thrown clear + * of its galaxy therefore holds what a globular inside one holds, which is the whole content of + * "it brought its own stars".

    + */ + private static final double INTERGALACTIC_CLUSTER_FIELD = 1d; + + private LocalField localFieldAt(long seed, long supX, long supY, long supZ) { long s = config.minSpacing; // The CONTAINING galaxy: a cluster inside a satellite belongs to the satellite, and its nucleus - // sits at the satellite's own centre. + // sits at the satellite's own centre. Absent out in the void, where a cluster may still sit. Optional galaxy = galaxies.galaxyContainingSector(seed, supX * s + s / 2L, supY * s + s / 2L, supZ * s + s / 2L); - if (!galaxy.isPresent()) { - return 1; - } - Optional cluster = clusters.clusterAt(seed, galaxy.get(), supX, supY, supZ); + Optional cluster = clusters.clusterAt(seed, galaxy.orElse(null), supX, supY, supZ); if (!cluster.isPresent()) { - return 1; + return LocalField.PLAIN; } // A cluster cannot conjure room its coarse cell never had. Refining below the smallest cell a // system can be more than a lone star in would not make a dense cluster — it would make a // field of bare stars, which is the opposite of the thing. A spacing too tight to refine is a // degenerate galaxy rather than an error, exactly as too tight a spacing already is. long ceiling = Math.max(1L, s / UniverseScale.MIN_LATTICE_EDGE_CELLS); - return (int) Math.max(1L, Math.min(cluster.get().subdivision(), ceiling)); + int k = (int) Math.max(1L, Math.min(cluster.get().subdivision(), ceiling)); + return new LocalField(k, galaxy.isPresent() ? 0d : INTERGALACTIC_CLUSTER_FIELD); } /** The lattice cell a sector triple falls in. */ @@ -870,14 +1070,15 @@ private Lattice latticeAt(long seed, long sectorX, long sectorY, long sectorZ) { long supX = Math.floorDiv(sectorX, s); long supY = Math.floorDiv(sectorY, s); long supZ = Math.floorDiv(sectorZ, s); - int k = subdivisionAt(seed, supX, supY, supZ); + LocalField local = localFieldAt(seed, supX, supY, supZ); + int k = local.subdivision; if (k <= 1) { - return Lattice.of(supX, supY, supZ, 0L, 0L, 0L, 1, s); + return Lattice.of(supX, supY, supZ, 0L, 0L, 0L, 1, s, local.ownField); } return Lattice.of(supX, supY, supZ, subIndex(Math.floorMod(sectorX, s), s, k), subIndex(Math.floorMod(sectorY, s), s, k), - subIndex(Math.floorMod(sectorZ, s), s, k), k, s); + subIndex(Math.floorMod(sectorZ, s), s, k), k, s, local.ownField); } /** @@ -896,27 +1097,12 @@ private static long subIndex(long offsetInCoarse, long coarseEdge, int k) { return Math.min((long) k - 1L, Math.max(0L, index)); } - /** - * How dense the CONTAINING galaxy is at this sector triple, in {@code [0, 1]} — zero in the void and - * zero past every galaxy's declared edge. - * - *

    The galaxy cell is a coarse reading of the sector, so this is O(1) and needs no stored index: - * every point belongs to exactly one galaxy cell, and that cell holds a primary galaxy, its - * satellites, or nothing.

    - * - *

    The containing galaxy, not the cube's owner. A cube holds a primary and its retinue, so - * reading the owner's profile at a point inside a satellite would answer zero — and the satellites - * would be named, addressable and completely empty of stars.

    - */ - private double galaxyProfileAt(long seed, long sectorX, long sectorY, long sectorZ) { - Optional galaxy = galaxies.galaxyContainingSector(seed, sectorX, sectorY, sectorZ); - if (!galaxy.isPresent()) { - return 0d; - } - return galaxy.get().densityAtSector(sectorX, sectorY, sectorZ); - } + // galaxyProfileAt — the CONTAINING galaxy's density at a sector triple — moved into + // GalaxyField.materialAtSector, which answers it together with the ejecta halo out in the void. + // The two are one walk over the cube, and that walk runs once per lattice cell of every placement + // query, so leaving this here would have meant resolving the cube twice for every star in the game. - private StarSystem fabricate(long seed, Lattice lattice) { + private PlanetarySystem fabricate(long seed, Lattice lattice) { long supX = lattice.lowX; long supY = lattice.lowY; long supZ = lattice.lowZ; @@ -926,11 +1112,11 @@ private StarSystem fabricate(long seed, Lattice lattice) { StellarBody star = new StellarBody(); star.setTemperature(type.temperature); star.setSize((float) (type.minSize + sizeFrac * (type.maxSize - type.minSize))); - int primaryId = syntheticId(seed, supX, supY, supZ); + int primaryId = syntheticId(seed, supX, supY, supZ, SALT_ID); star.setId(primaryId); star.setName("PGS-" + supX + "." + supY + "." + supZ); // procedurally-generated system addCompanions(seed, supX, supY, supZ, star, primaryId); - return new StarSystem(star); + return PlanetarySystem.ofStar(star); } /** @@ -1039,18 +1225,22 @@ private GalaxyGenConfig.StarType pickType(long h) { * The primary's synthetic id: negative, so it can never collide with a catalogued star id * ({@code 0..N}) or a dim id, and spaced {@link #ID_SLOTS_PER_SYSTEM} apart so a system's * companions have ids of their own below it that belong to no other system. + * + * @param salt which population is being named. A rogue system draws from the SAME space as a star + * through a different stream, so its id is as distinct from a star's as two stars' ids + * are from each other — no more and no less */ - private static int syntheticId(long seed, long supX, long supY, long supZ) { - long slot = Math.floorMod(CellHash.of(seed, supX, supY, supZ, SALT_ID), + private static int syntheticId(long seed, long supX, long supY, long supZ, long salt) { + long slot = Math.floorMod(CellHash.of(seed, supX, supY, supZ, salt), SYNTHETIC_ID_RANGE / ID_SLOTS_PER_SYSTEM); return -(1 + (int) (slot * ID_SLOTS_PER_SYSTEM)); } private static final class Generated { final GalacticCoord cell; - final StarSystem system; + final PlanetarySystem system; - Generated(GalacticCoord cell, StarSystem system) { + Generated(GalacticCoord cell, PlanetarySystem system) { this.cell = cell; this.system = system; } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/EmptyGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/EmptyGalaxyGenerator.java index 174d7820a..f0d9896ea 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/EmptyGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/EmptyGalaxyGenerator.java @@ -15,12 +15,12 @@ public final class EmptyGalaxyGenerator implements IGalaxyGenerator { @Override - public Optional systemAt(long seed, GalacticCoord coord) { + public Optional systemAt(long seed, GalacticCoord coord) { return Optional.empty(); } @Override - public Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { + public Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { return Collections.emptyMap(); } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java index 8e2b6bea2..4d2be1481 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java @@ -6,7 +6,7 @@ * One galaxy: a seated object with a centre, a type, a size, an orientation and a density profile. * *

    It is a VALUE, produced on demand from {@code (seed, galaxy cell)} and stored nowhere — exactly - * as a {@link StarSystem} is. Nothing here is persisted and no coordinate carries a galaxy index; the + * as a {@link PlanetarySystem} is. Nothing here is persisted and no coordinate carries a galaxy index; the * index is {@code sector / galaxySpacing}, a derived grouping of the sector space that already * exists (see {@link GalaxyField#galaxyIndex}).

    * @@ -53,6 +53,28 @@ public final class Galaxy { private static final double REFERENCE_LEVEL = Math.exp(-UniverseScale.HOME_GALAXY_ORIGIN_FRACTION / DISC_SCALE_FRACTION); + /** + * What {@link #densityAt} reads at a galaxy's own EDGE, in its plane. Derived from the profile + * rather than written down, and scale-free for the same reason every other length here is: the + * exponentials are in units of the radius, so this is one number for a galaxy of any size and of + * either profile. + * + *

    It is the anchor the {@linkplain #ejectaDensityAt ejecta halo} hangs from, which is what makes + * the void's population a statement about the galaxies that threw it out rather than a second + * field with its own normalisation.

    + */ + public static final double EDGE_LEVEL = Math.exp(-1d / DISC_SCALE_FRACTION) / REFERENCE_LEVEL; + + /** + * How steeply a galaxy's ejecta thins outside it, as a power of the distance in radii. + * + *

    Three, because that is what a population thrown out over a Hubble time and spread through a + * growing volume comes to — the same slope the outer parts of a real stellar halo and the + * intracluster light are measured at. It is not the disc's exponential: an exponential in units of + * the radius is dead within a few of them, and the void is twenty-five across.

    + */ + private static final double EJECTA_FALLOFF = 3d; + private final long cellX; private final long cellY; private final long cellZ; @@ -283,6 +305,36 @@ public double densityAtSector(long sectorX, long sectorY, long sectorZ) { offsetLy(sectorZ, centre.sectorZ())); } + /** + * How dense this galaxy's UNBOUND material is at a point {@code (dx, dy, dz)} light years from its + * centre — the planets and stars it has thrown out — on the same scale as {@link #densityAt}. + * + *

    Zero INSIDE the radius, and that is a division of labour rather than a claim that a galaxy + * ejects nothing into itself: inside its own sphere the bound profile is what says how much + * material is at a point, and adding a second term there would double-count the same stars.

    + * + *

    Outside, it falls as {@code (R/r)³} from {@link #EDGE_LEVEL} — anchored at the edge, so a big + * galaxy fills far more of the void than a dwarf and neither needs a normalisation of its own. It + * is ISOTROPIC while the disc is not: ejection randomises a direction long before a body has + * crossed the void, so a spiral's poles are not a dead cone. The step at the radius is therefore + * real, and it is at the one surface this layer already declares as a boundary — the surface where + * the frame flips and where the star field stops dead.

    + */ + public double ejectaDensityAt(double dxLy, double dyLy, double dzLy) { + double r = Math.sqrt(dxLy * dxLy + dyLy * dyLy + dzLy * dzLy); + if (r <= radiusLy) { + return 0d; + } + return EDGE_LEVEL * Math.pow(radiusLy / r, EJECTA_FALLOFF); + } + + /** The ejecta halo read at a cell name — the form the generator asks in. */ + public double ejectaDensityAtSector(long sectorX, long sectorY, long sectorZ) { + return ejectaDensityAt(offsetLy(sectorX, centre.sectorX()), + offsetLy(sectorY, centre.sectorY()), + offsetLy(sectorZ, centre.sectorZ())); + } + /** * The arms' contribution as a multiplier in {@code (0, 1]}, normalised so a point ON an arm scores * 1 and the disc between them is dimmer. A type with no arms scores 1 everywhere, so a smooth disc diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java index e549bee5d..479fcd30c 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java @@ -215,6 +215,81 @@ public Optional galaxyContaining(long seed, GalacticCoord cell) { return galaxyContainingSector(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ()); } + /** + * How much stellar material stands at one sector triple, split into the part that is BOUND to a + * galaxy and the part that is not. + * + *

    The two are asked together because they are answered by the same walk over the cube, and that + * walk is the expensive thing on the placement path — it runs once per lattice cell of every + * placement query, so resolving the cube twice would double the cost of every star in the game.

    + */ + public static final class Material { + + /** Nothing here: the cube is empty, or a point too far from anything in it. */ + public static final Material NONE = new Material(0d, 0d); + + /** + * The density of the galaxy this point is INSIDE, on {@link Galaxy#densityAt}'s scale, or zero + * out in the void. What decides where stars form. + */ + public final double bound; + /** + * The density of the cube's galaxies' ejecta at this point — what they have thrown out and no + * longer hold. It is the void's whole population, and it is zero inside a galaxy, where the + * bound profile already accounts for every body standing there. + */ + public final double unbound; + + Material(double bound, double unbound) { + this.bound = bound > 0d ? bound : 0d; + this.unbound = unbound > 0d ? unbound : 0d; + } + + /** Everything at this point, bound or not — what a population that does not need a star sees. */ + public double total() { + return bound + unbound; + } + } + + /** + * The bound and unbound material at a sector triple, resolved in ONE pass over the cube's galaxies. + * + *

    Supersedes reading the profile alone. A caller that only wants to place a STAR reads + * {@link Material#bound} and gets exactly what it got before; the void's own population reads + * {@link Material#total()}, which is what makes the intergalactic content a consequence of the + * galaxies rather than a second field seated by its own rule.

    + */ + public Material materialAtSector(long seed, long sectorX, long sectorY, long sectorZ) { + Optional owner = galaxyOwningSector(seed, sectorX, sectorY, sectorZ); + if (!owner.isPresent()) { + // A cube with no galaxy has thrown nothing out: the deepest void, and genuinely empty. + return Material.NONE; + } + Galaxy primary = owner.get(); + if (primary.containsSector(sectorX, sectorY, sectorZ)) { + // Inside the primary, and the retinue is never drawn here. A satellite is at most 0.3 R + // across and sits one to three DIAMETERS out, so the strongest halo one can cast anywhere + // inside its primary is a couple of percent of what the primary's own disc reads there — + // and this is the hottest path in the layer, taken for every cell of the shipped galaxy. + return new Material(primary.densityAtSector(sectorX, sectorY, sectorZ), 0d); + } + double unbound = primary.ejectaDensityAtSector(sectorX, sectorY, sectorZ); + if (!withinRetinueReach(primary, sectorX, sectorY, sectorZ)) { + return new Material(0d, unbound); // past the retinue: only the primary's own halo reaches + } + for (Galaxy satellite : satellitesOf(seed, primary)) { + if (satellite.containsSector(sectorX, sectorY, sectorZ)) { + return new Material(satellite.densityAtSector(sectorX, sectorY, sectorZ), unbound); + } + // The strongest halo, never the sum: two overlapping haloes are one region of thrown-out + // material counted twice, and adding them would make the gap between two dwarfs read + // denser than either dwarf's own edge. + unbound = Math.max(unbound, + satellite.ejectaDensityAtSector(sectorX, sectorY, sectorZ)); + } + return new Material(0d, unbound); + } + /** * The satellites of {@code primary} — drawn from {@code (seed, its cell, ordinal)}, stored nowhere, * exactly as the primary itself is. Empty for a type that keeps none, and for a satellite: the diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index 8d5237b20..6fdb1e86e 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -167,15 +167,27 @@ public static final class ClusterType { * cluster and its cloud are one object at two ages.

    */ public final double nebulaFraction; + /** + * Whether a cluster of this type holds itself together well enough to survive OUTSIDE a + * galaxy. Only these are seated in the intergalactic void. + * + *

    It is not a gameplay switch but the property that decides the question: a globular is + * bound tightly enough to have outlived its own galaxy's mergers and is routinely found far + * out in a halo, while an open cluster disperses in a few hundred million years and a + * molecular cloud never was bound at all. Something thrown clear of a galaxy has the whole + * crossing to fall apart in, so only the bound one arrives.

    + */ + public final boolean selfBound; public final int weight; public ClusterType(String name, int subdivision, double minRadiusLy, double maxRadiusLy, - double nebulaFraction, int weight) { + double nebulaFraction, boolean selfBound, int weight) { this.name = (name == null || name.isEmpty()) ? "CLUSTER" : name; this.subdivision = Math.max(1, subdivision); this.minRadiusLy = Math.max(0.01d, minRadiusLy); this.maxRadiusLy = Math.max(this.minRadiusLy, maxRadiusLy); this.nebulaFraction = Math.min(1d, Math.max(0d, nebulaFraction)); + this.selfBound = selfBound; this.weight = Math.max(1, weight); } } @@ -334,13 +346,13 @@ private static List defaultGalaxyTypes() { */ private static List defaultClusterTypes() { List l = new ArrayList<>(); - // name k radius band (ly) gas weight + // name k radius band (ly) gas bound weight // A molecular cloud is a cluster whose stars have not formed: it refines nothing (k = 1) and // is all gas. That it drops out of the SAME table as the others is the point — a cloud, a // young cluster and an ancient one are one sequence, not three features. - l.add(new ClusterType("Molecular Cloud", 1, 10d, 30d, 1.0d, 60)); - l.add(new ClusterType("Open Cluster", 4, 5d, 15d, 0.55d, 80)); - l.add(new ClusterType("Globular Cluster", 14, 20d, 40d, 0d, 20)); + l.add(new ClusterType("Molecular Cloud", 1, 10d, 30d, 1.0d, false, 60)); + l.add(new ClusterType("Open Cluster", 4, 5d, 15d, 0.55d, false, 80)); + l.add(new ClusterType("Globular Cluster", 14, 20d, 40d, 0d, true, 20)); return Collections.unmodifiableList(l); } @@ -348,7 +360,7 @@ private static List defaultClusterTypes() { * The cluster every galaxy has at its own centre — the richest one, and no special case: it is a * cluster like the others, drawn at the galaxy's centre instead of on the cluster lattice. */ - public static final ClusterType NUCLEUS = new ClusterType("Nucleus", 215, 4d, 8d, 0.4d, 1); + public static final ClusterType NUCLEUS = new ClusterType("Nucleus", 215, 4d, 8d, 0.4d, true, 1); /** Edge of the cube that holds at most one cluster, in light years. */ public static final double CLUSTER_SPACING_LY = 300d; @@ -356,6 +368,65 @@ private static List defaultClusterTypes() { /** Fraction of those cubes that hold a cluster, before the galaxy's own profile scales it. */ public static final double CLUSTER_DENSITY = 0.35d; + // ─── The unbound population ──────────────────────────────────────────────── + // What a lattice cube holds when no star was seated in it. Stated as constants beside the cluster + // tier's and for the same reason: it is a whole tier's worth of numbers, none of them yet ratified, + // and neither tier is authorable from today. + + /** + * A weighted ROGUE archetype — what an unbound seat turns out to hold. The fourth table of the + * shape {@link StarType} / {@link GalaxyType} / {@link ClusterType} use, and it exists for the + * same reason they do: relative abundance is a WEIGHT, so "by falling abundance" is a + * property of the table rather than a rule somewhere in the generator, and adding a kind of + * unbound object later is one row instead of a fourth occupancy knob. + */ + public static final class RogueType { + public final String name; + /** What is actually seated — the {@link SystemBodyKind} the anchor's primary body carries. */ + public final SystemBodyKind primaryKind; + public final int weight; + + public RogueType(String name, SystemBodyKind primaryKind, int weight) { + this.name = (name == null || name.isEmpty()) ? "ROGUE" : name; + this.primaryKind = (primaryKind == null) ? SystemBodyKind.ROGUE_PLANET : primaryKind; + this.weight = Math.max(1, weight); + } + } + + /** + * How many unbound seats the lattice draws for each STAR it draws, at the same point. + * + *

    It multiplies the same {@link #density} against the same profile, which is what makes it an + * occupancy FACTOR rather than a density of its own: everything already built — the super-cell + * partition, member-cell attribution, the survey's stride, the seat margins — keeps working + * untouched, and "more numerous than stars" is one number.

    + * + *

    Above one because free-floating worlds really do outnumber stars; at the LOW end of the + * observed band, which runs from comparable to some tens of times, because a lattice cube is a + * STAR's territory and seating a rogue in one claims rogues partition space the way stars do. + * Measured at the stock density: in a sun-like neighbourhood this seats about as many rogue + * worlds as stars, because a cube the star draw already took is not offered twice.

    + */ + public static final double ROGUE_ABUNDANCE = 1.5d; + + /** + * The stock rogue table. A thrown-out WORLD is the ordinary case and a thrown-out STAR is the + * find: ejecting a star takes an encounter violent enough to unbind the heaviest thing in a + * system, while a planet is unbound by the ordinary jostling of the system it formed in. + * + *

    A rogue star is a {@link SystemBodyKind#STAR} and nothing else — rogue-ness is a statement + * about WHERE it stands, not about what it is, so it is seated as an ordinary system and gets an + * ordinary retinue. That is why finding one out here is an event: it is a whole system, in a place + * where a system has no business being.

    + */ + public static List defaultRogueTypes() { + List l = new ArrayList<>(); + // name what is seated weight + l.add(new RogueType("Rogue Planet", SystemBodyKind.ROGUE_PLANET, 200)); + l.add(new RogueType("Rogue Star", SystemBodyKind.STAR, 3)); + return Collections.unmodifiableList(l); + } + private static double clamp01(double v) { if (Double.isNaN(v) || v < 0d) { return 0d; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java index 03047807a..4522703a6 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java @@ -28,14 +28,14 @@ public interface IGalaxyGenerator { * @param coord an absolute galactic coordinate; implementations should treat it at cell granularity * @return the procedural system at {@code coord}'s cell, or empty for void space */ - Optional systemAt(long seed, GalacticCoord coord); + Optional systemAt(long seed, GalacticCoord coord); /** * Enumerate every procedural system whose cell falls within the inclusive sector box {@code [min, max]}. * * @return a map from each occupied cell-centre coordinate to its system (empty when the region is void) */ - Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max); + Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max); /** * The procedural CONTENT of the system at {@code systemCoord}'s cell — its star plus planets/moons/POIs diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java index 262b38c01..ada54a947 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java @@ -145,6 +145,26 @@ public final class PlanetDerivation { private static final double METALLICITY_SPAN = 1.25d; private static final double METALLICITY_BIAS = 1.3d; + /** + * The surface temperature of an Earth-gravity world lit by NOTHING, in kelvin: what its own + * internal heat alone holds it at. + * + *

    Measured rather than picked. Earth's geothermal flux is 0.087 W/m²; a black body radiating + * that sits at {@code (F/σ)^¼ = 35 K}. Since the flux a world leaks scales with its mass over its + * area, and {@code M/R²} is exactly the surface gravity this derivation already computes, a + * starless world's temperature is {@code 35 K · g^¼} — one law, anchored on a real measurement, + * reusing a quantity that is already there rather than introducing a second size-to-heat + * relation.

    + * + *

    What it does not model: a young giant is far hotter than this, because most of its + * heat is gravitational contraction rather than leftover formation heat — Jupiter's own flux is + * sixty times Earth's, and it would come out at 124 K rather than the 45 K this gives. That is an + * age term, and nothing in this layer knows a body's age.

    + */ + private static final double RESIDUAL_TEMPERATURE_K = 35d; + /** {@code T ∝ F^¼} for a black body, and the flux goes as the gravity. */ + private static final double RESIDUAL_TEMPERATURE_EXPONENT = 0.25d; + private PlanetDerivation() { } @@ -312,6 +332,75 @@ public static BodyProfile derive(long seed, GalacticCoord anchor, GalacticCoord rings, metallicity, terrain, spin); } + /** + * The full profile of a world with NO STAR — a {@link SystemBodyKind#ROGUE_PLANET}, the commonest + * thing there is to meet in the intergalactic void. + * + *

    Half of {@link #derive}'s order simply does not apply, and that is the interesting part rather + * than a gap to be filled with defaults. There is no metallicity inherited from a parent star, no + * orbital radius, no insolation, no snow line to sit inside or outside of, and no tidal lock. What + * is left is the world's own bulk and its own leftover heat, so a rogue is derived from those and + * from nothing else.

    + * + *

    Its atmosphere is on the ground. A rocky rogue sits at a few tens of kelvin, where every + * volatile it ever had is frozen solid, so it reads at minimum pressure however well its gravity + * could have held a gas — the retention law answers "could it keep this gas hot" and the answer here + * is that there is no gas left to keep. A body massive enough to have accreted hydrogen keeps it, + * because hydrogen does not freeze at these temperatures, and that is the one case that comes out + * thick.

    + * + *

    One kind, whatever its bulk. A rogue that accreted like a giant is still a + * {@code ROGUE_PLANET} and not a {@link SystemBodyKind#GAS_GIANT}: that kind exists to say "a + * destination with a dimension and no surface", which is a statement about realization, and a rogue + * is not realized into a dimension yet. Its bulk is in the profile for anything that wants it.

    + * + * @param variant disambiguates bodies SHARING a cell — the rogue itself is 0 and its moons follow + */ + public static BodyProfile deriveRogue(long seed, GalacticCoord bodyCell, int variant) { + GalacticCoord key = bodyCell.cellCentre(); + // Its own draw, because it has no star to have inherited one from. A rogue formed in some + // system and carries that system's metals; which system is not a thing this layer can know. + double metallicity = metallicityOf(seed, key); + // Colder than any snow line, by construction — so the giant roll is the outer-zone one, which + // is the same law every other body past the frost line is drawn by rather than a rate of its own. + boolean bulky = isGiantAt(seed, key, variant, 0); + + double radius = radiusOf(seed, key, variant, bulky, false); + double mass = massOf(seed, key, variant, radius, bulky); + int gravityPercent = gravityPercentOf(mass, radius); + int pressure = bulky ? DimensionProperties.MAX_ATM_PRESSURE : DimensionProperties.MIN_ATM_PRESSURE; + int temperature = residualTemperature(mass, radius); + + PlanetTypePreset preset = PlanetTypes.drawType(pressure, temperature, gravityPercent, bulky, + CellHash.ofBody(seed, key, variant, SALT_TYPE)); + TerrainOption terrain = PlanetTypes.drawTerrain(preset, + CellHash.ofBody(seed, key, variant, SALT_TERRAIN)); + + boolean rings = CellHash.norm(CellHash.ofBody(seed, key, variant, SALT_RINGS)) + < (bulky ? RING_CHANCE_GIANT : RING_CHANCE_ROCKY); + int spin = rotationalPeriodOf(seed, key, variant, bulky); + + // No oxygen: free oxygen is biology, and it is also a GAS — a world whose air is lying on it as + // ice has none of either. No tidal lock: there is nothing to be locked to. + return new BodyProfile(SystemBodyKind.ROGUE_PLANET, + preset == null ? PlanetTypes.UNCLASSIFIED : preset.name(), preset, + SystemBody.ORBIT_UNKNOWN, mass, radius, gravityPercent, pressure, temperature, + false, false, rings, metallicity, terrain, spin); + } + + /** + * What a world with no star sits at, in kelvin: its own internal heat and nothing else. + * + *

    {@code 35 K · g^¼}, with {@code g = M/R²} in Earth units — see + * {@link #RESIDUAL_TEMPERATURE_K} for where the anchor comes from and what it leaves out.

    + */ + public static int residualTemperature(double massEarths, double radiusEarths) { + double gravity = massEarths / Math.max(1e-6d, radiusEarths * radiusEarths); + double kelvin = RESIDUAL_TEMPERATURE_K + * Math.pow(Math.max(1e-6d, gravity), RESIDUAL_TEMPERATURE_EXPONENT); + return (int) Math.max(1L, Math.round(kelvin)); + } + /** * How long this body takes to turn once, in ticks. * diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetarySystem.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetarySystem.java new file mode 100644 index 000000000..7c14274e9 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetarySystem.java @@ -0,0 +1,107 @@ +package zmaster587.advancedRocketry.universe; + +import java.util.Optional; + +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; + +/** + * An immutable query-time handle to what stands at one anchor cell, returned by the + * {@link UniverseRegistry} and {@link IGalaxyGenerator}. It carries deliberately no coordinate: + * a system is LOCATION-AGNOSTIC and its galactic address is owned solely by the registry + * (universe-model.md §2/§10). + * + *

    An anchor holds a PRIMARY BODY, and the primary need not be a star

    + *

    Most of them are: a star, its planets and its companion sub-stars, reusing the existing + * {@link StellarBody} content object. Out in the intergalactic void the commonest thing there is to + * meet is a world that was thrown out of the system it formed in, and it anchors a system of its own — + * it may keep moons, it has an address, and a telescope finds it exactly as it finds a star.

    + * + *

    So {@link #primaryKind()} says WHAT is here and {@link #star()} is an {@link Optional}, which is + * the point of the shape: a caller has to decide what it does about a system with no star instead of + * receiving a {@code null} or — worse — a 30 K, zero-radius {@code StellarBody} whose arithmetic comes + * out right while its name is a lie. The alternative shapes were both rejected for that reason: a + * nullable star hides the decision, and a rogue path of its own would duplicate the whole + * {@code coord → system → bodies} chain.

    + * + *

    Identity is the system's int id — the primary star's id where the primary IS a star, so a whole + * multi-star system shares one id and sub-stars mirror the primary.

    + */ +public final class PlanetarySystem { + + private final SystemBodyKind primaryKind; + /** The primary, when it is a star. {@code null} for a system whose primary is not one. */ + private final StellarBody star; + private final int id; + private final String name; + + private PlanetarySystem(SystemBodyKind primaryKind, StellarBody star, int id, String name) { + this.primaryKind = primaryKind; + this.star = star; + this.id = id; + this.name = name == null ? "" : name; + } + + /** The ordinary case: a system anchored on a star, with its planets and companions. */ + public static PlanetarySystem ofStar(StellarBody star) { + if (star == null) { + throw new NullPointerException("star"); + } + return new PlanetarySystem(SystemBodyKind.STAR, star, star.getId(), star.getName()); + } + + /** + * A system anchored on a starless world — a {@link SystemBodyKind#ROGUE_PLANET}. + * + *

    It carries an id and a name and nothing else, because there is nothing else to carry: a + * rogue's physics is derived from {@code (seed, cell)} by {@link PlanetDerivation} exactly as + * every other procedural world's is, and it has no star whose temperature or size anything here + * would have to remember.

    + */ + public static PlanetarySystem ofRogue(int id, String name) { + return new PlanetarySystem(SystemBodyKind.ROGUE_PLANET, null, id, name); + } + + /** What stands at this system's anchor — {@link SystemBodyKind#STAR} or a starless world. */ + public SystemBodyKind primaryKind() { + return primaryKind; + } + + /** + * The reused content object — the primary star plus its planets and companion sub-stars — or empty + * when this system's primary is not a star. + */ + public Optional star() { + return Optional.ofNullable(star); + } + + /** The system id (the primary star's id, where the primary is a star). */ + public int systemId() { + return id; + } + + /** What this system is called — the star's name, or the rogue's designation. */ + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PlanetarySystem)) { + return false; + } + return id == ((PlanetarySystem) o).id; + } + + @Override + public int hashCode() { + return id; + } + + @Override + public String toString() { + return "PlanetarySystem[" + primaryKind + " id=" + id + ", name=" + name + "]"; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/StarSystem.java b/src/main/java/zmaster587/advancedRocketry/universe/StarSystem.java deleted file mode 100644 index 81d24c0e0..000000000 --- a/src/main/java/zmaster587/advancedRocketry/universe/StarSystem.java +++ /dev/null @@ -1,56 +0,0 @@ -package zmaster587.advancedRocketry.universe; - -import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; - -/** - * An immutable query-time handle to a star system, returned by the {@link UniverseRegistry} and - * {@link IGalaxyGenerator}. It is a thin wrapper over the existing {@link StellarBody} content object - * (star + its planets + companion sub-stars) and deliberately carries no coordinate: a system is - * LOCATION-AGNOSTIC and its galactic address is owned solely by the registry (universe-model.md §2/§10). - * - *

    Identity is the system's int star-id (a whole multi-star system shares one id — sub-stars mirror the - * primary). This is the stable return type downstream tasks (generation, content/POIs, discovery) build on; - * they can grow richer accessors here without reshaping the registry's persistent index.

    - */ -public final class StarSystem { - - private final StellarBody star; - - public StarSystem(StellarBody star) { - if (star == null) { - throw new NullPointerException("star"); - } - this.star = star; - } - - /** The reused content object: the primary star plus its planets and companion sub-stars. */ - public StellarBody star() { - return star; - } - - /** The system id (== the primary star's id). */ - public int starId() { - return star.getId(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof StarSystem)) { - return false; - } - return starId() == ((StarSystem) o).starId(); - } - - @Override - public int hashCode() { - return star.getId(); - } - - @Override - public String toString() { - return "StarSystem[id=" + star.getId() + ", name=" + star.getName() + "]"; - } -} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java index 0464b90d1..21fd80524 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemBody.java @@ -246,7 +246,8 @@ public boolean isDescendTarget() { */ public boolean definesFrame() { return kind == SystemBodyKind.STAR || kind == SystemBodyKind.PLANET - || kind == SystemBodyKind.GAS_GIANT || kind == SystemBodyKind.ASTEROID_BELT; + || kind == SystemBodyKind.GAS_GIANT || kind == SystemBodyKind.ASTEROID_BELT + || kind == SystemBodyKind.ROGUE_PLANET; } /** diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemBodyKind.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemBodyKind.java index 67bda217b..a985f563f 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemBodyKind.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemBodyKind.java @@ -19,10 +19,38 @@ public enum SystemBodyKind { * rather than a {@link #PLANET}. Appended last on purpose: this ordinal travels on the render wire * ({@code PacketSystemBodiesSync}), so the existing kinds keep the numbers they already had. */ - GAS_GIANT; + GAS_GIANT, + /** + * A world with no star: a planet that was thrown out of the system it formed in, and now stands + * alone as the PRIMARY of its own cell. Its warmth is what is left of its own formation, so + * everything a star decides for an ordinary world — insolation, a year, a zone — it decides for + * itself or not at all. + * + *

    It is a kind of its own rather than a cold {@link #PLANET} around a cold {@link #STAR}, and + * that is the whole point of it existing: the arithmetic of a tiny 30 K star does come out right, + * and it would leave the model holding a {@code STAR} that is not a star. What a name is for is + * being true.

    + * + *

    Appended last, like {@link #GAS_GIANT} before it: this ordinal travels on the render wire + * ({@code PacketSystemBodiesSync}), so the existing kinds keep the numbers they already had.

    + */ + ROGUE_PLANET; - /** {@code true} for the body kinds that can back a walkable dimension (planets and moons). */ + /** + * {@code true} for the body kinds that can back a walkable dimension (planets and moons). + * + *

    A {@link #ROGUE_PLANET} is not among them yet, and that is a bound of the DIMENSION model + * rather than of the world. A realized dimension resolves its sky colour, its insolation, its + * year and its temperature through a star it is required to have, in some thirty unguarded places; + * a starless world is that model's own piece of work. Until it is done a rogue is a place a ship + * flies to and looks at, and the descent trigger never fires on one rather than failing at it.

    + */ public boolean canDescend() { return this == PLANET || this == MOON; } + + /** {@code true} for the kinds that are a WORLD — something with a surface, lit or not. */ + public boolean isWorld() { + return this == PLANET || this == MOON || this == ROGUE_PLANET; + } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java index 090eca4c1..a029b3a72 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java @@ -162,7 +162,7 @@ public static int resolveCell(UniverseRegistry registry, GalacticCoord cell, Cry } } if (!namedSomething) { - StarSystem system = registry.systemForCoord(anchor.get()).orElse(null); + PlanetarySystem system = registry.systemForCoord(anchor.get()).orElse(null); if (memory.record(entryForSystem(anchor.get(), system, observedTick))) { written++; } @@ -184,9 +184,11 @@ public static CrystalEntry entryFor(SystemBody body, long observedTick, IntFunct * A system with nothing the registry can enumerate: the address alone, so a pilot can still aim * at the light and go look. It names no body, because none has been resolved. */ - public static CrystalEntry entryForSystem(GalacticCoord coord, StarSystem system, long observedTick) { - String name = system != null && system.star() != null ? system.star().getName() : ""; - return new CrystalEntry(coord.cellCentre(), name, SystemBodyKind.STAR, InfoTier.TELESCOPE, - observedTick); + public static CrystalEntry entryForSystem(GalacticCoord coord, PlanetarySystem system, long observedTick) { + // The system's own name and its own PRIMARY KIND: a starless system recorded as a STAR would + // send a pilot out expecting a sun, and the address is the whole content of this entry. + String name = system == null ? "" : system.name(); + SystemBodyKind kind = system == null ? SystemBodyKind.STAR : system.primaryKind(); + return new CrystalEntry(coord.cellCentre(), name, kind, InfoTier.TELESCOPE, observedTick); } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java index 3389d13fa..20a75e80e 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java @@ -172,7 +172,7 @@ public static UniverseRegistry get(World world) { * a planet's own zone cell, or the void between bodies of one system — resolves to its owning system. * Resolution order: pinned → authored store → the procedural generator. Empty means void space. */ - public Optional systemForCoord(GalacticCoord coord) { + public Optional systemForCoord(GalacticCoord coord) { Optional anchor = anchorForCell(coord); if (!anchor.isPresent()) { return Optional.empty(); @@ -256,16 +256,17 @@ private static String neighbourSuperKey(GalacticCoord cell, int spacing, int dx, } /** The system AT a known anchor cell: pinned content → catalogued star → procedural generator. */ - private Optional systemAtAnchor(GalacticCoord anchor) { + private Optional systemAtAnchor(GalacticCoord anchor) { String key = anchor.cellKey(); PinnedSystem pinned = pinnedSystems.get(key); if (pinned != null) { - return Optional.of(new StarSystem(pinned.toStar())); + return Optional.of(pinned.toSystem()); } Integer id = byCell.get(key); if (id != null) { StellarBody star = starLookup.apply(id); - return star == null ? Optional.empty() : Optional.of(new StarSystem(star)); + return star == null ? Optional.empty() + : Optional.of(PlanetarySystem.ofStar(star)); } return generator.systemAt(worldSeed, anchor); } @@ -317,7 +318,7 @@ public boolean hasOverrideAt(GalacticCoord coord) { } /** Every stored system whose cell falls inside the inclusive sector box, merged over the generator. */ - public Map systemsInRegion(GalacticCoord min, GalacticCoord max) { + public Map systemsInRegion(GalacticCoord min, GalacticCoord max) { // Normalise the box once (per axis) so the generator and the override scan see the same ordered // bounds — a real generator is entitled to assume min <= max. GalacticCoord lo = GalacticCoord.ofSectorLocal( @@ -328,7 +329,7 @@ public Map systemsInRegion(GalacticCoord min, Galacti Math.max(min.sectorX(), max.sectorX()), Math.max(min.sectorY(), max.sectorY()), Math.max(min.sectorZ(), max.sectorZ()), 0L, 0L, 0L); - Map out = new HashMap<>(generator.systemsInRegion(worldSeed, lo, hi)); + Map out = new HashMap<>(generator.systemsInRegion(worldSeed, lo, hi)); for (Map.Entry e : byStar.entrySet()) { GalacticCoord c = e.getValue(); if (c.sectorX() >= lo.sectorX() && c.sectorX() <= hi.sectorX() @@ -336,7 +337,7 @@ public Map systemsInRegion(GalacticCoord min, Galacti && c.sectorZ() >= lo.sectorZ() && c.sectorZ() <= hi.sectorZ()) { StellarBody star = starLookup.apply(e.getKey()); if (star != null) { - out.put(c, new StarSystem(star)); // overrides win over any procedural entry at the same cell + out.put(c, PlanetarySystem.ofStar(star)); // overrides win over any procedural entry here } } } @@ -631,15 +632,22 @@ public boolean pinSystem(GalacticCoord coord) { if (byCell.containsKey(key)) { return false; // authored, or pinned already (pin places into byCell below) } - Optional sys = generator.systemAt(worldSeed, anchor); + Optional sys = generator.systemAt(worldSeed, anchor); if (!sys.isPresent()) { return false; } List bodies = new ArrayList<>(generator.bodiesFor(worldSeed, anchor)); - place(anchor, sys.get().starId()); - StellarBody star = sys.get().star(); - pinnedSystems.put(key, new PinnedSystem(sys.get().starId(), star.getTemperature(), star.getSize(), - star.getName(), bodies)); + place(anchor, sys.get().systemId()); + // A star's temperature and size are frozen HERE, because they are drawn values that a later + // seed or config edit would otherwise move under the planets already derived from them. A + // system with no star has neither, and freezing a zero for each would be inventing two + // properties it does not have — its primary's physics is derived from the cell like any + // other body's, and the cell is what the pin is keyed by. + PlanetarySystem system = sys.get(); + PinnedSystem snapshot = system.star().isPresent() + ? PinnedSystem.ofStar(system.systemId(), system.star().get(), bodies) + : PinnedSystem.ofRogue(system.systemId(), system.name(), bodies); + pinnedSystems.put(key, snapshot); markDirty(); return true; } @@ -652,6 +660,10 @@ public boolean pinSystem(GalacticCoord coord) { * frozen in the save, so a later seed or config edit cannot warm it up under the planets that were * derived from it. Realization needs this to materialize a body's physics, and the star it uses must * be the one the scan already described.

    + * + *

    Empty means two different things and a caller has to tell them apart: there is no system here + * at all, or there IS one and its primary is not a star (a rogue world out in the void). Ask + * {@link #systemForCoord} when the difference matters.

    */ public Optional starAt(GalacticCoord coord) { Optional anchorOpt = anchorForCell(coord); @@ -661,14 +673,14 @@ public Optional starAt(GalacticCoord coord) { GalacticCoord anchor = anchorOpt.get(); PinnedSystem pinned = pinnedSystems.get(anchor.cellKey()); if (pinned != null) { - return Optional.of(pinned.toStar()); + return pinned.toSystem().star(); } Integer id = byCell.get(anchor.cellKey()); if (id != null) { return Optional.ofNullable(starLookup.apply(id)); } - Optional sys = generator.systemAt(worldSeed, anchor); - return sys.isPresent() ? Optional.of(sys.get().star()) : Optional.empty(); + Optional sys = generator.systemAt(worldSeed, anchor); + return sys.isPresent() ? sys.get().star() : Optional.empty(); } /** @@ -1128,7 +1140,15 @@ public void readFromNBT(NBTTagCompound nbt) { for (int j = 0; j < bodyList.tagCount(); j++) { bodies.add(SystemBody.readFromNBT(bodyList.getCompoundTagAt(j))); } - pinnedSystems.put(anchor.cellKey(), new PinnedSystem(e.getInteger("starId"), + SystemBodyKind primaryKind = SystemBodyKind.STAR; + if (e.hasKey("primaryKind")) { + try { + primaryKind = SystemBodyKind.valueOf(e.getString("primaryKind")); + } catch (IllegalArgumentException ex) { + primaryKind = SystemBodyKind.STAR; // a kind this build does not know: read it as a star + } + } + pinnedSystems.put(anchor.cellKey(), PinnedSystem.read(e.getInteger("starId"), primaryKind, e.getInteger("temperature"), e.getFloat("size"), e.getString("name"), bodies)); } } @@ -1173,6 +1193,11 @@ public NBTTagCompound writeToNBT(NBTTagCompound nbt) { NBTTagCompound entry = new NBTTagCompound(); anchor.writeToNBT(entry); entry.setInteger("starId", p.starId); + // Written only when it is not a star, so a stellar system's snapshot is byte-identical to + // what it was before starless systems existed. + if (p.primaryKind != SystemBodyKind.STAR) { + entry.setString("primaryKind", p.primaryKind.name()); + } entry.setInteger("temperature", p.temperature); entry.setFloat("size", p.size); entry.setString("name", p.name == null ? "" : p.name); @@ -1189,29 +1214,58 @@ public NBTTagCompound writeToNBT(NBTTagCompound nbt) { return nbt; } - /** A pinned procedural system's content snapshot (A#1a pin-on-touch): fabricated star + body list. */ + /** + * A pinned procedural system's content snapshot (A#1a pin-on-touch): its primary's drawn + * properties plus its full body list. + * + *

    {@code primaryKind} is what a re-read reconstructs the system FROM, and it is stored rather + * than inferred from a zero temperature: a star that happens to be cold and a system that has no + * star are different facts, and telling them apart by their arithmetic is exactly the confusion + * the kind exists to end.

    + */ private static final class PinnedSystem { final int starId; + final SystemBodyKind primaryKind; final int temperature; final float size; final String name; final List bodies; - PinnedSystem(int starId, int temperature, float size, String name, List bodies) { + private PinnedSystem(int starId, SystemBodyKind primaryKind, int temperature, float size, + String name, List bodies) { this.starId = starId; + this.primaryKind = primaryKind; this.temperature = temperature; this.size = size; this.name = name; this.bodies = bodies; } - StellarBody toStar() { + static PinnedSystem ofStar(int starId, StellarBody star, List bodies) { + return new PinnedSystem(starId, SystemBodyKind.STAR, star.getTemperature(), star.getSize(), + star.getName(), bodies); + } + + /** A system anchored on a starless world: an id, a name, and nothing a star would have had. */ + static PinnedSystem ofRogue(int starId, String name, List bodies) { + return new PinnedSystem(starId, SystemBodyKind.ROGUE_PLANET, 0, 0f, name, bodies); + } + + static PinnedSystem read(int starId, SystemBodyKind primaryKind, int temperature, float size, + String name, List bodies) { + return new PinnedSystem(starId, primaryKind, temperature, size, name, bodies); + } + + PlanetarySystem toSystem() { + if (primaryKind != SystemBodyKind.STAR) { + return PlanetarySystem.ofRogue(starId, name); + } StellarBody star = new StellarBody(); star.setId(starId); star.setTemperature(temperature); star.setSize(size); star.setName(name); - return star; + return PlanetarySystem.ofStar(star); } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java index be953ba46..8a44e63f9 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java @@ -157,12 +157,17 @@ public void oneOrbitalDistanceMeansOneDistanceInBothFamilies() { // SWEEP for an occupied super-cell rather than demanding one particular cube. Occupancy is a // draw scaled by the galaxy's profile, so any single cube is a coin toss and a fixture that // insists on one is testing the coin. + // It must be a seat with a STAR: the comparison is between one authored planet's orbit and one + // procedural planet's, and a starless system has no orbits at all to compare with. Optional seat = Optional.empty(); - for (long i = 1; i <= 8 && !seat.isPresent(); i++) { - seat = gen.anchorAt(0xBEEFL, + for (long i = 1; i <= 16 && !seat.isPresent(); i++) { + Optional candidate = gen.anchorAt(0xBEEFL, GalacticCoord.ofSectorLocal(i * spacing, spacing, spacing, 0L, 0L, 0L)); + if (candidate.isPresent() && gen.systemAt(0xBEEFL, candidate.get()).get().star().isPresent()) { + seat = candidate; + } } - assertTrue("the fixture needs an occupied super-cell", seat.isPresent()); + assertTrue("the fixture needs an occupied super-cell with a star in it", seat.isPresent()); int compared = 0; for (SystemBody b : gen.bodiesFor(0xBEEFL, seat.get())) { if (b.kind() != SystemBodyKind.PLANET && b.kind() != SystemBodyKind.GAS_GIANT) { diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index 31f847dc4..764181f9f 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -14,7 +14,7 @@ import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; import zmaster587.advancedRocketry.universe.Galaxy; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; -import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; import zmaster587.advancedRocketry.universe.UniverseScale; @@ -92,14 +92,19 @@ public void systemAtIsDeterministic() { if (!anchor.isPresent()) { return; } - Optional a = gen.systemAt(SEED, anchor.get()); - Optional b = gen.systemAt(SEED, anchor.get()); + Optional a = gen.systemAt(SEED, anchor.get()); + Optional b = gen.systemAt(SEED, anchor.get()); assertTrue("an attributed anchor must point-resolve at " + anchor.get(), a.isPresent()); assertEquals("presence must be stable", a.isPresent(), b.isPresent()); - assertEquals("id stable", a.get().starId(), b.get().starId()); - assertEquals("temperature stable", a.get().star().getTemperature(), - b.get().star().getTemperature()); - assertEquals("size stable", a.get().star().getSize(), b.get().star().getSize(), 0f); + assertEquals("id stable", a.get().systemId(), b.get().systemId()); + assertEquals("primary kind stable", a.get().primaryKind(), b.get().primaryKind()); + assertEquals("star presence stable", a.get().star().isPresent(), b.get().star().isPresent()); + if (!a.get().star().isPresent()) { + return; // a starless system: it has no temperature or size to be stable + } + assertEquals("temperature stable", a.get().star().get().getTemperature(), + b.get().star().get().getTemperature()); + assertEquals("size stable", a.get().star().get().getSize(), b.get().star().get().getSize(), 0f); }); } @@ -186,23 +191,34 @@ public void aSeatIsNotConfinedToTheMiddleOfItsCube() { } @Test - public void starsStopAtTheirGalaxysDeclaredEdge() { - // The star field is the GALAXY's density profile, so where a galaxy ends the stars end. This - // is what an independent per-cell mask could not do: drawn above the percolation threshold it - // produced one unbounded sponge, with no edge to reach and no answer to "which galaxy is this". + public void starFormationStopsAtTheGalaxysDeclaredEdge() { + // The star field is the GALAXY's density profile, so where a galaxy ends, star FORMATION ends. + // This is what an independent per-cell mask could not do: drawn above the percolation threshold + // it produced one unbounded sponge, with no edge to reach and no answer to "which galaxy is + // this". + // + // It is star FORMATION and not "anything at all", and the distinction is the whole of the void + // content: what is out past the edge got there by being thrown, and the material that carries + // it is the ejecta halo rather than the profile. So the reading is the BOUND term — what a + // star needs to condense out of — and it is zero past the radius on the nose. // // Sampled against the home galaxy's OWN radius rather than a hard-coded distance: the radius // is drawn per seed, so a fixed number would be testing one draw. ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(1.0d, SPACING)); Galaxy home = gen.galaxies().home(SEED); - int inside = seatsInBlockAround(gen, 0L, 3); - long beyondEdge = UniverseScale.cellsForLightYears(home.radiusLy() * 1.5d); - int outside = seatsInBlockAround(gen, beyondEdge, 3); + assertTrue("the galaxy's core must hold stars (found " + seatsInBlockAround(gen, 0L, 3) + ")", + seatsInBlockAround(gen, 0L, 3) > 0); + assertTrue("inside the galaxy there must be material a star can form out of", + gen.galaxies().materialAtSector(SEED, 0L, 0L, 0L).bound > 0d); - assertTrue("the galaxy's core must hold stars (found " + inside + ")", inside > 0); - assertEquals("past the declared radius of " + (long) home.radiusLy() - + " ly there must be nothing", 0, outside); + long beyondEdge = UniverseScale.cellsForLightYears(home.radiusLy() * 1.5d); + for (long d = 0; d <= 3; d++) { + long sector = beyondEdge + d * SPACING; + assertEquals("past the declared radius of " + (long) home.radiusLy() + + " ly nothing may FORM, at " + sector, + 0d, gen.galaxies().materialAtSector(SEED, sector, 0L, 0L).bound, 0d); + } } @Test @@ -223,14 +239,14 @@ public void systemsInRegionAgreesWithSystemAt() { }); assertFalse("the sweep must find systems", byAttribution.isEmpty()); - Map region = gen.systemsInRegion(SEED, cell(-r, -r, -r), cell(r, r, r)); + Map region = gen.systemsInRegion(SEED, cell(-r, -r, -r), cell(r, r, r)); Set byRegion = new HashSet<>(); - for (Map.Entry e : region.entrySet()) { + for (Map.Entry e : region.entrySet()) { byRegion.add(e.getKey().cellKey()); // The enumerated cell must itself point-resolve to the same system. - Optional point = gen.systemAt(SEED, e.getKey()); + Optional point = gen.systemAt(SEED, e.getKey()); assertTrue("region cell " + e.getKey() + " must point-resolve", point.isPresent()); - assertEquals(point.get().starId(), e.getValue().starId()); + assertEquals(point.get().systemId(), e.getValue().systemId()); } assertTrue("every seat the sweep attributed must be enumerated by the region query", byRegion.containsAll(byAttribution)); @@ -240,8 +256,8 @@ public void systemsInRegionAgreesWithSystemAt() { public void systemsInRegionHandlesSwappedBounds() { ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(defaultsCfg()); long r = 2L * SPACING; - Map ordered = gen.systemsInRegion(SEED, cell(-r, -r, -r), cell(r, r, r)); - Map swapped = gen.systemsInRegion(SEED, cell(r, r, r), cell(-r, -r, -r)); + Map ordered = gen.systemsInRegion(SEED, cell(-r, -r, -r), cell(r, r, r)); + Map swapped = gen.systemsInRegion(SEED, cell(r, r, r), cell(-r, -r, -r)); assertEquals("swapped min/max must enumerate the same box", ordered.keySet(), swapped.keySet()); } @@ -290,8 +306,11 @@ public void starTypesAreDrawnFromTheConfiguredSetAndWeighted() { if (!anchor.isPresent()) { continue; } - StarSystem sys = gen.systemAt(SEED, anchor.get()).get(); - int temp = sys.star().getTemperature(); + PlanetarySystem sys = gen.systemAt(SEED, anchor.get()).get(); + if (!sys.star().isPresent()) { + continue; // a starless system draws no star archetype, which is this test's subject + } + int temp = sys.star().get().getTemperature(); seenTemps.add(Integer.toString(temp)); total++; if (temp == 50) { @@ -302,7 +321,7 @@ public void starTypesAreDrawnFromTheConfiguredSetAndWeighted() { other++; } // size must lie in the archetype's range - float size = sys.star().getSize(); + float size = sys.star().get().getSize(); if (temp == 50) { assertTrue(size >= 0.5f && size <= 1.0f); } else if (temp == 250) { @@ -323,7 +342,7 @@ public void proceduralSystemIdsAreNegative() { for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 2)) { sawAny = true; assertTrue("procedural systems must carry a synthetic negative id", - gen.systemAt(SEED, anchor).get().starId() < 0); + gen.systemAt(SEED, anchor).get().systemId() < 0); } assertTrue(sawAny); } @@ -361,9 +380,12 @@ public void hugeStarWeightsDoNotCollapseTheDistribution() { for (long x = -20; x <= 20; x++) { for (long y = -20; y <= 20; y++) { Optional anchor = gen.anchorAt(SEED, cell(x * SPACING, y * SPACING, 0)); - if (anchor.isPresent()) { - seenTemps.add(Integer.toString( - gen.systemAt(SEED, anchor.get()).get().star().getTemperature())); + if (!anchor.isPresent()) { + continue; + } + PlanetarySystem sys = gen.systemAt(SEED, anchor.get()).get(); + if (sys.star().isPresent()) { + seenTemps.add(Integer.toString(sys.star().get().getTemperature())); } } } @@ -380,6 +402,9 @@ public void proceduralBodiesGetTheirOwnCellsInsideTheSuperCell() { long s = config.minSpacing; boolean checkedAny = false; for (GalacticCoord anchor : anchors(gen, SEED, SPACING, 1)) { + if (!gen.systemAt(SEED, anchor).get().star().isPresent()) { + continue; // a starless system has no star at its anchor, which is what this pins + } checkedAny = true; List a = gen.bodiesFor(SEED, anchor); assertEquals("bodiesFor must be deterministic", a, gen.bodiesFor(SEED, anchor)); @@ -396,7 +421,7 @@ public void proceduralBodiesGetTheirOwnCellsInsideTheSuperCell() { Set systemStars = new HashSet<>(); systemStars.add(a.get(0).starId()); for (zmaster587.advancedRocketry.api.dimension.solar.StellarBody companion - : gen.systemAt(SEED, anchor).get().star().getSubStars()) { + : gen.systemAt(SEED, anchor).get().star().get().getSubStars()) { systemStars.add(companion.getId()); } @@ -488,9 +513,17 @@ public void tinySpacingDegeneratesIntoALoneStar() { } checkedAny = true; List bodies = gen.bodiesFor(SEED, c); - assertEquals("a one-cell neighbourhood can host exactly one real body", 1, bodies.size()); - assertEquals("and that body is the star", SystemBodyKind.STAR, bodies.get(0).kind()); - assertTrue("which holds the anchor cell", bodies.get(0).name().sameCell(c)); + int real = 0; + for (SystemBody body : bodies) { + assertTrue("nothing may escape the one cell this system has", + body.name().sameCell(c)); + if (body.definesFrame()) { + real++; + } + } + assertEquals("a one-cell neighbourhood can host exactly one real body", 1, real); + assertTrue("and that body is the system's primary", + bodies.get(0).definesFrame() && bodies.get(0).name().sameCell(c)); } assertTrue(checkedAny); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java index dd9546760..5d7a53613 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java @@ -14,7 +14,7 @@ import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; -import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.UniverseScale; import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; @@ -252,7 +252,7 @@ public void theInterstellarBandIsWhatTheGeneratorActuallyProduces() { long stride = 4L * GalaxyGenConfig.DEFAULT_MIN_SPACING; List legs = new ArrayList<>(); for (long seed = 1L; seed <= 20L; seed++) { - Map found = gen.systemsInRegion(seed, + Map found = gen.systemsInRegion(seed, cell(-stride, -stride, -stride), cell(stride, stride, stride)); GalacticCoord home = nearestTo(found.keySet(), cell(0L, 0L, 0L)); GalacticCoord neighbour = home == null ? null : nearestTo(found.keySet(), home); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java index 2f9514aa3..68fae8cf5 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java @@ -19,7 +19,7 @@ import zmaster587.advancedRocketry.universe.GalaxyField; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.LightYearVector; -import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.UniverseScale; import static org.junit.Assert.assertEquals; @@ -260,10 +260,13 @@ public void galaxyDensityIsUniformWhileTheCosmicWebIsANeutralConstant() { } @Test - public void theVoidBetweenGalaxiesHoldsNoSystems() { - // The generator's own view of the same fact: outside every galaxy the profile is zero, so the - // intergalactic void is what the profile leaves empty rather than a second rule someone has to - // remember to apply. + public void theVoidBetweenGalaxiesHoldsNothingThatFormedThere() { + // Outside every galaxy the BOUND profile is zero, so the intergalactic void is what the profile + // leaves empty rather than a second rule someone has to remember to apply. + // + // "Empty of stars", not "empty": what a ship meets out here is material the galaxies threw out, + // and that is the ejecta halo rather than the profile. This pins the half that has not moved — + // nothing CONDENSES out here — and VoidContentTest pins the half that has. ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(cfg(1.0d)); Galaxy home = gen.galaxies().home(77L); // Past the whole RETINUE, not just past the primary: a satellite sits one to three diameters @@ -274,8 +277,9 @@ public void theVoidBetweenGalaxiesHoldsNoSystems() { long spacing = GalaxyGenConfig.DEFAULT_MIN_SPACING; for (long i = 0; i < 40; i++) { GalacticCoord probe = GalacticCoord.ofSectorLocal(beyond + i * spacing, 0L, 0L, 0L, 0L, 0L); - assertFalse("a system turned up in intergalactic space at " + probe.cellKey(), - gen.anchorAt(77L, probe).isPresent()); + assertEquals("star-forming material turned up in intergalactic space at " + probe.cellKey(), + 0d, gen.galaxies().materialAtSector(77L, probe.sectorX(), probe.sectorY(), + probe.sectorZ()).bound, 0d); } } @@ -615,7 +619,7 @@ public void aSatelliteIsAPLACE_withStarsOfItsOwn() { // And the generator actually seats systems in it. long stride = config.minSpacing; - Map found = gen.systemsInRegion(seed, + Map found = gen.systemsInRegion(seed, GalacticCoord.ofSectorLocal(core.sectorX() - 3L * stride, core.sectorY() - 3L * stride, core.sectorZ() - 3L * stride, 0L, 0L, 0L), GalacticCoord.ofSectorLocal(core.sectorX() + 3L * stride, diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java index 94f7f57f8..ccb88269a 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java @@ -14,7 +14,7 @@ import zmaster587.advancedRocketry.space.GalacticCoord; import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; -import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.UniverseScale; import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; @@ -59,7 +59,7 @@ public void theNearestSystemIsFarEnoughToBeAJumpAndCloseEnoughToBeReached() { List ticks = new ArrayList<>(); List rows = new ArrayList<>(); for (long seed = 1L; seed <= 20L; seed++) { - Map found = gen.systemsInRegion(seed, + Map found = gen.systemsInRegion(seed, cell(-SEARCH_RADIUS_CELLS, -SEARCH_RADIUS_CELLS, -SEARCH_RADIUS_CELLS), cell(SEARCH_RADIUS_CELLS, SEARCH_RADIUS_CELLS, SEARCH_RADIUS_CELLS)); // The leg a PLAYER flies runs anchor to anchor: he sits in a system and jumps to another @@ -193,7 +193,7 @@ public void theMeasuredBandMatchesTheArithmeticItIsDerivedFrom() { /** The distance from the system nearest the origin to ITS nearest neighbour, in light years. */ private static Double nearestNeighbourLightYears(ClusteredGalaxyGenerator gen, long seed) { - Map found = gen.systemsInRegion(seed, + Map found = gen.systemsInRegion(seed, cell(-SEARCH_RADIUS_CELLS, -SEARCH_RADIUS_CELLS, -SEARCH_RADIUS_CELLS), cell(SEARCH_RADIUS_CELLS, SEARCH_RADIUS_CELLS, SEARCH_RADIUS_CELLS)); GalacticCoord home = nearestTo(found.keySet(), cell(0L, 0L, 0L)); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java index 92dd424f2..841c728d6 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java @@ -15,7 +15,7 @@ import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.IGalaxyGenerator; import zmaster587.advancedRocketry.universe.Nebula; -import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; import zmaster587.advancedRocketry.universe.TelescopeScan; @@ -64,12 +64,12 @@ private static StellarBody star(int id) { private static IGalaxyGenerator dustyBy(final double columnDensityLightYears) { return new IGalaxyGenerator() { @Override - public Optional systemAt(long seed, GalacticCoord coord) { + public Optional systemAt(long seed, GalacticCoord coord) { return Optional.empty(); } @Override - public Map systemsInRegion(long seed, GalacticCoord min, + public Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { return Collections.emptyMap(); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java index 3510e627b..deb1012b3 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaTest.java @@ -43,7 +43,7 @@ private static StarCluster clusterOfType(GalaxyGenConfig.ClusterType type) { } private static GalaxyGenConfig.ClusterType typeWithGas(double gas) { - return new GalaxyGenConfig.ClusterType("Test", 4, 5d, 15d, gas, 1); + return new GalaxyGenConfig.ClusterType("Test", 4, 5d, 15d, gas, false, 1); } // ─── The derivation ──────────────────────────────────────────────────────── diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java index b2560d788..d3f0d0130 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java @@ -14,7 +14,7 @@ import zmaster587.advancedRocketry.space.SkyNebulaeProducer; import zmaster587.advancedRocketry.universe.IGalaxyGenerator; import zmaster587.advancedRocketry.universe.Nebula; -import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.UniverseScale; import static org.junit.Assert.assertEquals; @@ -42,12 +42,12 @@ private static Nebula cloudAt(double xLy, double yLy, double zLy, double radiusL private static IGalaxyGenerator generatorOf(final List clouds) { return new IGalaxyGenerator() { @Override - public Optional systemAt(long seed, GalacticCoord coord) { + public Optional systemAt(long seed, GalacticCoord coord) { return Optional.empty(); } @Override - public Map systemsInRegion(long seed, GalacticCoord min, + public Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { return Collections.emptyMap(); } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java index 864efc27d..9b1d985b3 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java @@ -34,7 +34,7 @@ private static GalaxyGenConfig cfg() { } private static GalaxyGenConfig.ClusterType type(int k) { - return new GalaxyGenConfig.ClusterType("Test", k, 5d, 15d, 0.5d, 1); + return new GalaxyGenConfig.ClusterType("Test", k, 5d, 15d, 0.5d, false, 1); } // ─── The commensurate construction ───────────────────────────────────────── diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java index 5bead43de..0cbd774cf 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java @@ -17,6 +17,7 @@ import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.PlanetDerivation; +import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.PlanetTypes; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; @@ -67,7 +68,14 @@ private static ClusteredGalaxyGenerator gen(int minSpacing) { null, null)); } - /** Every occupied system anchor in a sweep of super-cells. */ + /** + * Every anchor in a sweep of super-cells that holds a system with a STAR. + * + *

    Starless systems are skipped, and the filter is the subject of this class rather than a + * convenience: a retinue is what orbits a star — a zone, a snow line, a belt at the outer edge of + * one — and a system with no star has none of those to get right. What a rogue keeps instead, and + * that it still honours one real body per cell, is {@code VoidContentTest}'s.

    + */ private static List anchors(ClusteredGalaxyGenerator g, long seed, int minSpacing, int supercells) { Set seen = new HashSet<>(); @@ -77,7 +85,11 @@ private static List anchors(ClusteredGalaxyGenerator g, long seed for (long sz = -supercells; sz <= supercells; sz++) { Optional a = g.anchorAt(seed, cell(sx * minSpacing, sy * minSpacing, sz * minSpacing)); - if (a.isPresent() && seen.add(a.get().cellKey())) { + if (!a.isPresent() || !seen.add(a.get().cellKey())) { + continue; + } + Optional sys = g.systemAt(seed, a.get()); + if (sys.isPresent() && sys.get().star().isPresent()) { out.add(a.get()); } } @@ -149,7 +161,7 @@ public void atTheShippedScaleASingleStarLosesNoBodyAtAll() { ClusteredGalaxyGenerator g = gen(SPACING); int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { - if (!g.systemAt(SEED, anchor).get().star().getSubStars().isEmpty()) { + if (!g.systemAt(SEED, anchor).get().star().get().getSubStars().isEmpty()) { continue; } int wanted = ClusteredGalaxyGenerator.retinueSize(SEED, anchor); @@ -175,7 +187,7 @@ public void aCrampedSystemDropsBodiesAndNeverMovesTheOnesItKeeps() { int droppedSomewhere = 0; int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, CRAMPED_SPACING, 2)) { - StellarBody star = g.systemAt(SEED, anchor).get().star(); + StellarBody star = g.systemAt(SEED, anchor).get().star().get(); int count = ClusteredGalaxyGenerator.retinueSize(SEED, anchor); Set drawn = new HashSet<>(); for (int i = 0; i < count; i++) { @@ -216,7 +228,7 @@ public void aCompanionCostsItsSystemTheWorldsItStandsAmong() { // universe into a failure about nothing. The rate itself (10/28 = 36 %) is what the // multiplicity contract says it should be. for (GalacticCoord anchor : anchors(g, SEED, SPACING, 3)) { - if (g.systemAt(SEED, anchor).get().star().getSubStars().isEmpty()) { + if (g.systemAt(SEED, anchor).get().star().get().getSubStars().isEmpty()) { continue; } multiple++; @@ -250,7 +262,7 @@ public void someSystemsHoldMoreThanOneStarAndMostDoNot() { int multiple = 0; int mostStars = 0; for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { - StellarBody star = g.systemAt(SEED, anchor).get().star(); + StellarBody star = g.systemAt(SEED, anchor).get().star().get(); int stars = 1 + star.getSubStars().size(); systems++; if (stars > 1) { @@ -273,7 +285,7 @@ public void everyStarOfASystemHasAnIdOfItsOwn() { ClusteredGalaxyGenerator g = gen(SPACING); int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { - StellarBody star = g.systemAt(SEED, anchor).get().star(); + StellarBody star = g.systemAt(SEED, anchor).get().star().get(); Set ids = new HashSet<>(); assertTrue(ids.add(star.getId())); for (StellarBody companion : star.getSubStars()) { @@ -297,7 +309,7 @@ public void aCompanionIsABodyOfItsSystemStandingAtItsOwnSeparation() { ClusteredGalaxyGenerator g = gen(SPACING); int checkedCompanions = 0; for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { - StellarBody star = g.systemAt(SEED, anchor).get().star(); + StellarBody star = g.systemAt(SEED, anchor).get().star().get(); if (star.getSubStars().isEmpty()) { continue; } @@ -334,7 +346,7 @@ public void noWorldSitsWhereAnotherStarWouldTearItAway() { ClusteredGalaxyGenerator g = gen(SPACING); int checked = 0; for (GalacticCoord anchor : anchors(g, SEED, SPACING, 2)) { - StellarBody star = g.systemAt(SEED, anchor).get().star(); + StellarBody star = g.systemAt(SEED, anchor).get().star().get(); if (star.getSubStars().isEmpty()) { continue; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java index 73479d685..a0d84acd6 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java @@ -384,14 +384,14 @@ private static final class CountingGenerator implements zmaster587.advancedRocke } @Override - public java.util.Optional systemAt( + public java.util.Optional systemAt( long seed, GalacticCoord coord) { queries++; return real.systemAt(seed, coord); } @Override - public java.util.Map + public java.util.Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { queries++; return real.systemsInRegion(seed, min, max); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java index 454708696..8597fd558 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java @@ -20,7 +20,7 @@ import zmaster587.advancedRocketry.universe.EmptyGalaxyGenerator; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.IGalaxyGenerator; -import zmaster587.advancedRocketry.universe.StarSystem; +import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; import zmaster587.advancedRocketry.universe.UniverseRegistry; @@ -258,22 +258,22 @@ public void systemForCoordPrefersStoredOverGenerator() { GalacticCoord placedCell = GalacticCoord.ofSectorLocal(5, 5, 5, 0, 0, 0); reg.place(placedCell, 42); - Optional atPlaced = reg.systemForCoord(placedCell); + Optional atPlaced = reg.systemForCoord(placedCell); assertTrue(atPlaced.isPresent()); - assertSame("stored placement must win over the generator", stored, atPlaced.get().star()); - assertEquals(42, atPlaced.get().starId()); + assertSame("stored placement must win over the generator", stored, atPlaced.get().star().get()); + assertEquals(42, atPlaced.get().systemId()); // A member cell of the stored anchor's super-cell attributes to the STORED system, not the // generator: an authored anchor owns every cell of its super-cell. - Optional nearStored = reg.systemForCoord(GalacticCoord.ofSectorLocal(6, 6, 6, 0, 0, 0)); + Optional nearStored = reg.systemForCoord(GalacticCoord.ofSectorLocal(6, 6, 6, 0, 0, 0)); assertTrue(nearStored.isPresent()); - assertEquals(42, nearStored.get().starId()); + assertEquals(42, nearStored.get().systemId()); // A cell in a DIFFERENT super-cell falls through to the generator. - Optional farAway = reg.systemForCoord( + Optional farAway = reg.systemForCoord( GalacticCoord.ofSectorLocal(ANOTHER_SUPER_CELL, ANOTHER_SUPER_CELL, ANOTHER_SUPER_CELL, 0, 0, 0)); assertTrue(farAway.isPresent()); - assertEquals(777, farAway.get().starId()); + assertEquals(777, farAway.get().systemId()); } @Test @@ -291,10 +291,10 @@ public void memberCellResolvesToItsOwningProceduralSystem() { GalacticCoord anchor = null; SystemBody planet = null; for (long sup = 0; sup < 8 && planet == null; sup++) { - Optional sys = reg.systemForCoord( + Optional sys = reg.systemForCoord( GalacticCoord.ofSectorLocal(sup * cfg.minSpacing, 0, 0, 0, 0, 0)); - if (!sys.isPresent()) { - continue; + if (!sys.isPresent() || !sys.get().star().isPresent()) { + continue; // a starless system has no star body to be the anchor of this comparison } for (SystemBody b : reg.systemBodiesAt( GalacticCoord.ofSectorLocal(sup * cfg.minSpacing, 0, 0, 0, 0, 0))) { @@ -310,9 +310,9 @@ public void memberCellResolvesToItsOwningProceduralSystem() { assertFalse("the sampled body must sit in its OWN cell", planet.name().sameCell(anchor)); // The body's cell resolves to the same system (member attribution). - Optional atBody = reg.systemForCoord(planet.name()); + Optional atBody = reg.systemForCoord(planet.name()); assertTrue(atBody.isPresent()); - assertEquals(planet.starId(), atBody.get().starId()); + assertEquals(planet.starId(), atBody.get().systemId()); // Zone read at the body's cell returns the body; at the anchor it returns the star, not the body. List zone = reg.bodiesAt(planet.name()); @@ -349,7 +349,7 @@ public void pinOnTouchSnapshotsAProceduralSystemAgainstSeedChange() { } assertNotNull("need an occupied procedural super-cell", anchor); - int starIdBefore = reg.systemForCoord(anchor).get().starId(); + int starIdBefore = reg.systemForCoord(anchor).get().systemId(); List bodiesBefore = reg.systemBodiesAt(anchor); // TOUCH: pin the system (addPoi would do the same implicitly). @@ -359,7 +359,7 @@ public void pinOnTouchSnapshotsAProceduralSystemAgainstSeedChange() { // A config/seed change (the drift scenario) must NOT move or reshape the pinned system… reg.bindWorldSeed(999_999L); assertEquals("pinned system survives a seed change", starIdBefore, - reg.systemForCoord(anchor).get().starId()); + reg.systemForCoord(anchor).get().systemId()); assertEquals("pinned bodies survive a seed change", bodiesBefore, reg.systemBodiesAt(anchor)); // …and the pin round-trips through NBT (reads from the save, not the generator or catalogue). @@ -369,7 +369,7 @@ public void pinOnTouchSnapshotsAProceduralSystemAgainstSeedChange() { round.readFromNBT(tag); round.bindWorldSeed(999_999L); assertTrue(round.systemForCoord(anchor).isPresent()); - assertEquals(starIdBefore, round.systemForCoord(anchor).get().starId()); + assertEquals(starIdBefore, round.systemForCoord(anchor).get().systemId()); assertEquals(bodiesBefore, round.systemBodiesAt(anchor)); } @@ -383,9 +383,9 @@ public void systemForCoordIsEmptyOnVoidCellWithDefaultGenerator() { public void systemsAreLocationAgnostic() { // The coordinate is obtainable ONLY from the registry; the system handle exposes no coordinate. StellarBody body = star(9); - StarSystem sys = new StarSystem(body); - assertEquals(9, sys.starId()); - assertSame(body, sys.star()); + PlanetarySystem sys = PlanetarySystem.ofStar(body); + assertEquals(9, sys.systemId()); + assertSame(body, sys.star().get()); UniverseRegistry reg = new UniverseRegistry(); assertFalse("an unregistered system has no coord", reg.coordForStar(body).isPresent()); @@ -761,14 +761,14 @@ private static final class AllClaimingGenerator implements IGalaxyGenerator { } @Override - public Optional systemAt(long seed, GalacticCoord coord) { - return Optional.of(new StarSystem(body)); + public Optional systemAt(long seed, GalacticCoord coord) { + return Optional.of(PlanetarySystem.ofStar(body)); } @Override - public Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { - Map m = new HashMap<>(); - m.put(min.cellCentre(), new StarSystem(body)); + public Map systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { + Map m = new HashMap<>(); + m.put(min.cellCentre(), PlanetarySystem.ofStar(body)); return m; } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/VoidContentTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/VoidContentTest.java new file mode 100644 index 000000000..97da89de7 --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/VoidContentTest.java @@ -0,0 +1,357 @@ +package zmaster587.advancedRocketry.test.unit; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.BodyProfile; +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Galaxy; +import zmaster587.advancedRocketry.universe.GalaxyField; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.PlanetDerivation; +import zmaster587.advancedRocketry.universe.PlanetarySystem; +import zmaster587.advancedRocketry.universe.StarCluster; +import zmaster587.advancedRocketry.universe.SystemBody; +import zmaster587.advancedRocketry.universe.SystemBodyKind; +import zmaster587.advancedRocketry.universe.UniverseScale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for what is out there in the INTERGALACTIC VOID: rogue worlds, rogue stars, and the + * globulars that were thrown clear of a galaxy. Pure JUnit; no MC bootstrap. + * + *

    The void's content is not a second placement rule. It is the SAME star lattice, drawn a second + * time against the SAME material function — the galaxy's own profile where there is a galaxy, and its + * ejecta halo where there is not. So the contracts here are about that one function's shape and about + * what a starless system is, never about the numbers either of them happens to be tuned to.

    + * + *

    Sampling is by SUPER-CELL and the sweeps are large. Out past a galaxy's edge the occupancy + * is percent-scale, so a sweep of a few dozen cubes finds nothing whatever the model says. Where a + * count would need thousands of samples to be stable, the test reads the PROFILE instead, which is + * exact and is the thing the contract is actually about.

    + */ +public class VoidContentTest { + + private static final long SEED = 0x5EEDF00DL; + private static final int SPACING = GalaxyGenConfig.DEFAULT_MIN_SPACING; + + /** Every cube occupied at the galaxy's densest point, so a void sweep is not fighting the draw too. */ + private static ClusteredGalaxyGenerator gen() { + return new ClusteredGalaxyGenerator(new GalaxyGenConfig(SPACING, 1.0d, + GalaxyGenConfig.DEFAULT_GALAXY_SPACING, GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, + null, null)); + } + + private static GalacticCoord cell(long sx, long sy, long sz) { + return GalacticCoord.ofSectorLocal(sx, sy, sz, 0L, 0L, 0L); + } + + /** + * A sector on the +X axis, {@code radii} of the home galaxy's radius out from its CENTRE. + * + *

    Measured from the centre and in units of the radius, because the radius is drawn per seed: a + * fixed light-year distance would be inside the galaxy on one seed and deep in the void on the + * next, and the test would be pinning that draw rather than the model.

    + */ + private static long xAt(Galaxy home, double radii) { + return home.centre().sectorX() + UniverseScale.cellsForLightYears(home.radiusLy() * radii); + } + + // ─── The material function: what the void is made of ─────────────────────── + + @Test + public void pastAGalaxysEdgeTheMaterialIsUnboundAndOnlyUnbound() { + // The split IS the model: inside a galaxy, material a star can condense out of; outside it, + // material that was thrown out of one and can only be arrived at. + ClusteredGalaxyGenerator gen = gen(); + Galaxy home = gen.galaxies().home(SEED); + GalaxyField field = gen.galaxies(); + + GalaxyField.Material inside = field.materialAtSector(SEED, home.centre().sectorX(), + home.centre().sectorY(), home.centre().sectorZ()); + assertTrue("a galaxy's own centre must hold bound material", inside.bound > 0d); + assertEquals("and none of it is unbound: the profile already counts every body there", + 0d, inside.unbound, 0d); + + // 1.5 radii out is outside the primary and out of every satellite's reach as well: a satellite + // is seated at least one full DIAMETER out and is at most 0.3 R across, so the nearest surface + // any of them can present is 1.7 R. + GalaxyField.Material outside = field.materialAtSector(SEED, xAt(home, 1.5d), + home.centre().sectorY(), home.centre().sectorZ()); + assertEquals("nothing FORMS past the declared radius", 0d, outside.bound, 0d); + assertTrue("but the void is not empty: the galaxy's ejecta reaches into it", + outside.unbound > 0d); + } + + @Test + public void theEjectaHaloThinsWithDistanceFromItsGalaxy() { + // A power law anchored at the edge, so the void has a GRADIENT: a ship stepping out of a galaxy + // meets worlds often and meets them less and less the further out it goes. A flat floor would + // have made a galaxy's doorstep and the middle of nowhere read identically. + ClusteredGalaxyGenerator gen = gen(); + Galaxy home = gen.galaxies().home(SEED); + GalaxyField field = gen.galaxies(); + + double previous = Double.MAX_VALUE; + for (double radii : new double[] {1.5d, 3d, 6d, 12d, 24d}) { + double here = field.materialAtSector(SEED, xAt(home, radii), home.centre().sectorY(), + home.centre().sectorZ()).unbound; + assertTrue("the halo must be present at " + radii + " radii", here > 0d); + assertTrue("the halo must thin outwards: " + here + " at " + radii + + " radii is not below " + previous, here < previous); + previous = here; + } + } + + @Test + public void aGalaxyCubeWithNoGalaxyInItIsCompletelyEmpty() { + // The deepest void, and it is genuinely nothing: the population out here is what the cube's own + // galaxies threw out, so a cube that never held one has thrown out nothing. That is what makes + // half the universe a place only a galactic drive can cross, rather than a uniform fog. + ClusteredGalaxyGenerator gen = gen(); + GalaxyField field = gen.galaxies(); + long spacing = GalaxyGenConfig.DEFAULT_GALAXY_SPACING; + + boolean checkedAny = false; + for (long g = 1; g <= 40 && !checkedAny; g++) { + if (field.galaxyAtIndex(SEED, g, 0L, 0L).isPresent()) { + continue; + } + checkedAny = true; + long sector = g * spacing + spacing / 2L; + GalaxyField.Material material = field.materialAtSector(SEED, sector, 0L, 0L); + assertEquals("an empty galaxy cube holds no bound material", 0d, material.bound, 0d); + assertEquals("nor any ejecta: nothing was ever here to throw it", 0d, material.unbound, 0d); + assertFalse("and therefore no system at all", + gen.anchorAt(SEED, cell(sector, 0L, 0L)).isPresent()); + } + assertTrue("the sweep must find a galaxy cube that is empty", checkedAny); + } + + // ─── What the second draw actually seats ─────────────────────────────────── + + @Test + public void theVoidHoldsSystemsAndTheyAreMostlyStarless() { + // The whole point of the feature: out past the edge a ship meets things, and what it meets is + // overwhelmingly a world with no sun. A rogue STAR is drawn from the same table at a small + // weight, which is what makes finding a whole lit system out here an event rather than routine. + ClusteredGalaxyGenerator gen = gen(); + Galaxy home = gen.galaxies().home(SEED); + long x0 = xAt(home, 1.5d); + + int starless = 0; + int lit = 0; + Set seen = new HashSet<>(); + for (long i = -6; i <= 6; i++) { + for (long j = -6; j <= 6; j++) { + for (long k = -6; k <= 6; k++) { + GalacticCoord probe = cell(x0 + i * SPACING, + home.centre().sectorY() + j * SPACING, + home.centre().sectorZ() + k * SPACING); + Optional anchor = gen.anchorAt(SEED, probe); + if (!anchor.isPresent() || !seen.add(anchor.get().cellKey())) { + continue; + } + if (gen.systemAt(SEED, anchor.get()).get().star().isPresent()) { + lit++; + } else { + starless++; + } + } + } + } + assertTrue("the void just outside a galaxy must hold systems (found none in 13³ cubes)", + starless + lit > 0); + assertTrue("what it holds must be mostly starless (starless " + starless + ", lit " + lit + ")", + starless > lit); + } + + @Test + public void aStarlessSystemNamesItselfAndIsFoundLikeAnyOther() { + // Registered as an anchor is the whole of "discoverable": a survey resolves a look through the + // system that OWNS the cell, so being registered IS being findable, and a rogue needed no + // discovery mechanism of its own. + ClusteredGalaxyGenerator gen = gen(); + GalacticCoord anchor = aRogueAnchor(gen); + PlanetarySystem system = gen.systemAt(SEED, anchor).get(); + + assertEquals("its primary is a starless world", SystemBodyKind.ROGUE_PLANET, + system.primaryKind()); + assertFalse("and it has no star to be asked for", system.star().isPresent()); + assertFalse("it carries a designation of its own", system.name().isEmpty()); + assertTrue("its id is synthetic, so it can never collide with a catalogued star or a dim", + system.systemId() < 0); + + // Member attribution works exactly as it does for a star: an ordinary cell beside the seat + // resolves back to it, which is what lets a ship arrive anywhere near one and know where it is. + Optional viaMember = gen.anchorAt(SEED, + anchor.plusLocal(GalacticCoord.CELL, 0L, 0L)); + assertTrue("a member cell must attribute to the rogue's anchor", viaMember.isPresent()); + assertTrue(viaMember.get().sameCell(anchor)); + } + + @Test + public void aStarlessSystemIsTheWorldItsMoonsAndNothingElse() { + // No belt and no companion, and neither is an omission: a belt is material that never accreted + // in a star's own well, and a companion is another star. What survives being thrown out of a + // system is the world and whatever was held tightly enough to come with it. + ClusteredGalaxyGenerator gen = gen(); + GalacticCoord anchor = aRogueAnchor(gen); + List bodies = gen.bodiesFor(SEED, anchor); + + assertFalse("a rogue system must have bodies", bodies.isEmpty()); + assertEquals("the first is the rogue itself, at the anchor", SystemBodyKind.ROGUE_PLANET, + bodies.get(0).kind()); + assertTrue(bodies.get(0).name().sameCell(anchor)); + + int framesDefined = 0; + for (SystemBody body : bodies) { + assertTrue("everything a rogue keeps shares its one cell", body.name().sameCell(anchor)); + assertTrue("nothing here is a star or a belt", + body.kind() == SystemBodyKind.ROGUE_PLANET || body.kind() == SystemBodyKind.MOON); + assertFalse("a rogue is not a descend target yet, so neither is anything in its system", + body.isDescendTarget()); + if (body.definesFrame()) { + framesDefined++; + } + } + assertEquals("AT MOST ONE REAL BODY PER CELL holds for a rogue too, moons excepted", + 1, framesDefined); + assertEquals("and it is deterministic", bodies, gen.bodiesFor(SEED, anchor)); + } + + // ─── What a starless world IS ────────────────────────────────────────────── + + @Test + public void aRogueIsWarmedByItselfAndByNothingElse() { + // Its temperature is leftover formation heat leaking out through its own surface, so it is a + // function of the body and of nothing external — which is the design opportunity in having no + // star, rather than a gap where the insolation used to be. + ClusteredGalaxyGenerator gen = gen(); + GalacticCoord anchor = aRogueAnchor(gen); + BodyProfile profile = PlanetDerivation.deriveRogue(SEED, anchor, 0); + + assertEquals(SystemBodyKind.ROGUE_PLANET, profile.kind()); + assertTrue("a starless world is colder than anything a star lights: " + profile.temperatureKelvin() + + " K", profile.temperatureKelvin() < 200); + assertTrue("but it is not at absolute zero either", profile.temperatureKelvin() > 0); + assertFalse("free oxygen is biology AND a gas; a world whose air is ice on the ground has neither", + profile.hasOxygen()); + assertFalse("there is nothing for it to be tidally locked TO", profile.tidallyLocked()); + assertEquals("and no orbit of its own", SystemBody.ORBIT_UNKNOWN, profile.orbitalDistance()); + assertEquals("deterministic, like every other derived body", + profile.temperatureKelvin(), + PlanetDerivation.deriveRogue(SEED, anchor, 0).temperatureKelvin()); + } + + @Test + public void aHeavierRogueRunsWarmerThanALighterOne() { + // The law and not the draw: heat leaks out in proportion to the mass behind each square metre + // of surface, which is the same M/R² this derivation already calls gravity. A test that pinned + // the constant would be pinning a balance number; what is a contract is the DIRECTION. + int earthLike = PlanetDerivation.residualTemperature(1d, 1d); + int heavy = PlanetDerivation.residualTemperature(10d, 1.5d); + int feather = PlanetDerivation.residualTemperature(0.05d, 0.5d); + + assertTrue("a heavier world holds more of its own heat: " + heavy + " K vs " + earthLike + " K", + heavy > earthLike); + assertTrue("and a small light one has almost none left: " + feather + " K vs " + earthLike + " K", + feather < earthLike); + } + + // ─── Clusters that were thrown clear of a galaxy ──────────────────────────── + + @Test + public void anIntergalacticClusterIsSeatedAndItIsSelfBound() { + // Seating one outside a galaxy used to be refused by construction, on the reasoning that there + // would be no stars out there to gather. A cluster does not gather the field — it arrived with + // its own — so the refusal is lifted, and what is lifted with it is only the types that could + // actually survive the crossing. + ClusteredGalaxyGenerator gen = gen(); + Galaxy home = gen.galaxies().home(SEED); + long clusterSpacing = gen.clusters().spacingSuperCells(); + long baseIndex = Math.floorDiv(Math.floorDiv(xAt(home, 1.5d), (long) SPACING), clusterSpacing); + + StarCluster found = null; + for (long i = 0; i < 400 && found == null; i++) { + Optional cluster = gen.clusters().clusterAtIndex(SEED, null, + baseIndex + i, 0L, 0L); + if (cluster.isPresent()) { + found = cluster.get(); + } + } + assertNotNull("the void must be able to hold a cluster at all", found); + assertTrue("and only a SELF-BOUND one: an open cluster or a cloud would have dispersed on the " + + "way out. Got " + found.type().name, found.type().selfBound); + } + + @Test + public void aClusterOutsideAGalaxyStillHoldsItsStars() { + // The reason the type filter is not the whole story. A cluster's density is expressed as a + // CONTRAST against what surrounds it, and out here what surrounds it is nearly nothing — so + // k³ times nearly nothing would have produced a globular that was named, addressable and + // completely empty. It brings its own field, so it holds what a globular holds. + ClusteredGalaxyGenerator gen = gen(); + Galaxy home = gen.galaxies().home(SEED); + long clusterSpacing = gen.clusters().spacingSuperCells(); + long baseIndex = Math.floorDiv(Math.floorDiv(xAt(home, 1.5d), (long) SPACING), clusterSpacing); + + StarCluster cluster = null; + for (long i = 0; i < 400 && cluster == null; i++) { + Optional c = gen.clusters().clusterAtIndex(SEED, null, baseIndex + i, 0L, 0L); + if (c.isPresent()) { + cluster = c.get(); + } + } + assertNotNull(cluster); + + // Probe the coarse super-cell the cluster's own core sits in, against one well outside it. + long inX = cluster.centreSuperX(); + long inY = cluster.centreSuperY(); + long inZ = cluster.centreSuperZ(); + int inside = 0; + for (long d = 0; d < 4; d++) { + if (gen.anchorAt(SEED, cell((inX + d) * SPACING, inY * SPACING, inZ * SPACING)).isPresent()) { + inside++; + } + } + assertTrue("a cluster out in the void must still be full of systems (found " + inside + + " in 4 probes at its core)", inside > 0); + } + + /** + * The anchor of the first STARLESS system found just outside the home galaxy. + * + *

    Swept rather than named: which cube holds one is a draw, so a fixture that insisted on one + * particular cube would be testing the draw. It fails loudly if the sweep comes up dry, because a + * silent skip here would make every test that uses it vacuous.

    + */ + private static GalacticCoord aRogueAnchor(ClusteredGalaxyGenerator gen) { + Galaxy home = gen.galaxies().home(SEED); + long x0 = xAt(home, 1.5d); + for (long i = -6; i <= 6; i++) { + for (long j = -6; j <= 6; j++) { + for (long k = -6; k <= 6; k++) { + Optional anchor = gen.anchorAt(SEED, + cell(x0 + i * SPACING, home.centre().sectorY() + j * SPACING, + home.centre().sectorZ() + k * SPACING)); + if (anchor.isPresent() + && !gen.systemAt(SEED, anchor.get()).get().star().isPresent()) { + return anchor.get(); + } + } + } + } + throw new AssertionError("no starless system anywhere in 13³ super-cells just outside the home " + + "galaxy - the void draw is not producing anything"); + } +} From f1af7e10690e3692a128b6d47207e9a3c237f598 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Tue, 18 Aug 2026 08:23:44 +0300 Subject: [PATCH 36/42] fix: a scanned world is the world you land on - make averageTemperature private with one setter and a pure getter - recompute only where an input changes, not on every read - admit a planet type at the temperature its own albedo gives it - end the derivation on the albedo the dimension model will read --- .../sub/planet/PlanetGenerateCommand.java | 2 +- .../command/test/TestProbeCommand.java | 8 +-- .../dimension/DimensionManager.java | 6 +- .../dimension/DimensionProperties.java | 59 ++++++++++++++----- .../universe/PlanetDerivation.java | 31 +++++++--- .../universe/PlanetRealizer.java | 6 +- .../universe/PlanetTypes.java | 42 ++++++++++--- .../util/XMLPlanetLoader.java | 11 +++- .../test/unit/PlanetDerivationTest.java | 47 ++++++++++++++- 9 files changed, 167 insertions(+), 45 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java b/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java index d77dfd883..f242bbc42 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java @@ -99,7 +99,7 @@ public void execute(MinecraftServer server, ICommandSender sender, String[] args props.setBulk(profile.massEarths(), profile.radiusEarths()); props.gravitationalMultiplier = profile.gravityPercent() / 100f; props.setAtmosphereDensityDirect(profile.pressure()); - props.averageTemperature = profile.temperatureKelvin(); + props.setAverageTemp(profile.temperatureKelvin()); props.initDefaultAttributes(); if (moon) { props.setParentPlanet(parent); diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index ce1f5573b..8d36ba618 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -5238,7 +5238,7 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] + "\",\"orbitalDist\":" + props.getOrbitalDist() + ",\"mass\":" + props.getMass() + ",\"radius\":" + props.getRadius() + ",\"gravity\":" + Math.round(props.getGravitationalMultiplier() * 100f) + ",\"pressure\":" - + props.getAtmosphereDensity() + ",\"temperature\":" + props.averageTemperature + + props.getAtmosphereDensity() + ",\"temperature\":" + props.getAverageTemp() + ",\"oxygen\":" + props.hasOxygen + ",\"locked\":" + props.isTidallyLocked() + ",\"metallicity\":" + props.getMetallicity() + ",\"gasGiant\":" + props.isGasGiant() + ",\"terrainSource\":\"" + props.getTerrainSource() @@ -5998,7 +5998,7 @@ private void handlePlanet(ICommandSender sender, String[] args) { info.put("thunderStartLength", props.getThunderStartLength()); info.put("rainMarker", props.getRainMarker()); info.put("thunderMarker", props.getThunderMarker()); - info.put("averageTemperature", props.averageTemperature); + info.put("averageTemperature", props.getAverageTemp()); info.put("genType", props.getGenType()); IBlockState ocean = props.getOceanBlock(); // null is meaningful — vanilla water fallback — so emit explicitly. @@ -6022,11 +6022,11 @@ private void handlePlanet(ICommandSender sender, String[] args) { + ",\"kelvin\":" + kelvin + "}"); return; } - props.averageTemperature = kelvin; + props.setAverageTemp(kelvin); Map out = new LinkedHashMap<>(); out.put("ok", true); out.put("dim", dim); - out.put("averageTemperature", props.averageTemperature); + out.put("averageTemperature", props.getAverageTemp()); out.put("hasOxygen", props.hasOxygen); out.put("atmosphereDensity", props.getAtmosphereDensity()); out.put("atmosphere", props.getAtmosphere().getUnlocalizedName()); diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java index debe237b5..745da5e7b 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java @@ -87,7 +87,7 @@ public DimensionManager() { overworldProperties = new DimensionProperties(0); overworldProperties.setAtmosphereDensityDirect(100); //Temperature in Kelvin, 286 is 13 Degrees C - overworldProperties.averageTemperature = 286; + overworldProperties.setAverageTemp(286); overworldProperties.gravitationalMultiplier = 1f; overworldProperties.orbitalDist = 100; overworldProperties.skyColor = new float[]{1f, 1f, 1f}; @@ -97,7 +97,7 @@ public DimensionManager() { defaultSpaceDimensionProperties = new DimensionProperties(SpaceObjectManager.WARPDIMID, false); defaultSpaceDimensionProperties.setAtmosphereDensityDirect(0); - defaultSpaceDimensionProperties.averageTemperature = 0; + defaultSpaceDimensionProperties.setAverageTemp(0); defaultSpaceDimensionProperties.gravitationalMultiplier = 0.1f; defaultSpaceDimensionProperties.orbitalDist = 100; defaultSpaceDimensionProperties.skyColor = new float[]{0f, 0f, 0f}; @@ -772,7 +772,7 @@ public void createAndLoadDimensions(boolean resetFromXml) { if (zmaster587.advancedRocketry.api.ARConfiguration.getCurrentConfig().MoonId != Constants.INVALID_PLANET) { DimensionProperties dimensionProperties = new DimensionProperties(zmaster587.advancedRocketry.api.ARConfiguration.getCurrentConfig().MoonId); dimensionProperties.setAtmosphereDensityDirect(0); - dimensionProperties.averageTemperature = 20; + dimensionProperties.setAverageTemp(20); dimensionProperties.rotationalPeriod = 128000; dimensionProperties.gravitationalMultiplier = .166f; //Actual moon value dimensionProperties.setName("Luna"); diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java index dadb75c82..75c8e7afa 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java @@ -103,8 +103,17 @@ private static float clampFeatureFrequencyMultiplier(float multiplier) { //Used in solar panels public double peakInsolationMultiplier; public double peakInsolationMultiplierWithoutAtmosphere; - //Stored in Kelvin - public int averageTemperature; + /** + * This world's surface temperature in KELVIN — a DERIVED quantity, cached here. + * + *

    Private, and it is the point. It used to be a public field that + * {@link #getAverageTemp()} ASSIGNED on every call, while a dozen readers inside this class took + * the field directly — so what any of them saw depended on whether anything had happened to call + * the accessor first, and the value NBT had faithfully restored was discarded by the first read + * after a load. One door in ({@link #setAverageTemp}), one door out, and the recompute now happens + * where an INPUT changes rather than where the answer is asked for.

    + */ + private int averageTemperature; public int rotationalPeriod; //Stored in radians public double orbitTheta; @@ -1027,6 +1036,13 @@ public void setAtmosphereDensity(int atmosphereDensity) { int prevAtm = this.atmosphereDensity; this.atmosphereDensity = atmosphereDensity; + // The ONE input that changes while a world is in play — the terraformer thickens or thins the + // air, and the greenhouse term moves with it. Everything else a temperature is derived from + // (the stars, the orbit, the albedo) is fixed when the world is materialized, and is STATED + // through setAverageTemp rather than recomputed here: a load path that recomputed would be + // running before its own inputs had all been read. + recalculateTemperature(); + load_terraforming_helper(true); @@ -2428,22 +2444,33 @@ public void writeToNBT(NBTTagCompound nbt) { */ @Override public int getAverageTemp() { - averageTemperature = AstronomicalBodyHelper.getAverageTemperature(this.getStar(), - this.getSolarOrbitalDistance(), this.getAtmosphereDensity(), this.albedo); - - /* - int temp = averageTemperature; - float pressure = (float) (atmosphereDensity + 1) / (float) 100; - pressure = (float) Math.max(0.01, pressure); - float water_can_exist_value = 400; - float planetvalue = temp / pressure; + return averageTemperature; + } - if (planetvalue < water_can_exist_value) { - water_can_exist = true; - } else water_can_exist = false; - */ + /** + * State this world's surface temperature, in KELVIN. + * + *

    The one door in. A caller that MATERIALIZES a world — realization from a derived profile, an + * XML load, a probe fixture — states the number it already has; everything else changes an INPUT + * and lets {@link #recalculateTemperature()} follow.

    + */ + public void setAverageTemp(int kelvin) { + this.averageTemperature = kelvin; + } - return averageTemperature; + /** + * Recompute the surface temperature from this world's current inputs — its stars, its orbit, its + * atmosphere and its albedo. + * + *

    Called where an input CHANGES, never where the answer is read. On a world that was + * materialized from a derived profile this is a no-op by construction: {@code PlanetDerivation} + * ends on this same call with this same albedo, so a recompute reproduces the number a telescope + * already reported. That equality is the contract, and it is what stopped a scanned world from + * cooling down on the way there.

    + */ + public void recalculateTemperature() { + setAverageTemp(AstronomicalBodyHelper.getAverageTemperature(getStar(), + getSolarOrbitalDistance(), getAtmosphereDensity(), albedo)); } public IBlockState getOceanBlock() { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java index ada54a947..82388c6ce 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java @@ -1,5 +1,7 @@ package zmaster587.advancedRocketry.universe; +import java.util.function.DoubleToIntFunction; + import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; import zmaster587.advancedRocketry.dimension.DimensionProperties; import zmaster587.advancedRocketry.space.GalacticCoord; @@ -307,11 +309,23 @@ public static BodyProfile derive(long seed, GalacticCoord anchor, GalacticCoord double mass = massOf(seed, key, variant, radius, giant); int gravityPercent = gravityPercentOf(mass, radius); int pressure = pressureOf(seed, key, variant, mass, radius, bareTemp, giant); - int temperature = AstronomicalBodyHelper.getAverageTemperature(star, - Math.max(1, orbitalDistance), pressure); - - PlanetTypePreset preset = PlanetTypes.drawType(pressure, temperature, gravityPercent, giant, - CellHash.ofBody(seed, key, variant, SALT_TYPE)); + // A world's ALBEDO is a property of its surface, its surface is what its TYPE says it is, and + // the type is admitted by temperature — so the temperature is not one number here but a + // FUNCTION of albedo, and each candidate type is admitted at the temperature the world would + // have if it were that type. Evaluated once per candidate; nothing iterates, and the physics + // stays here rather than moving into the table. + // + // While this was a single neutral-albedo reading, the derivation and the dimension model + // answered one question with two numbers: a `greenhouse` world (albedo 0.75) was reported + // 22.7 % warmer than it turned out to be and an `ice` world 13 % (ledger #289). + final int orbit = Math.max(1, orbitalDistance); + DoubleToIntFunction temperatureForAlbedo = + albedo -> AstronomicalBodyHelper.getAverageTemperature(star, orbit, pressure, albedo); + + PlanetTypePreset preset = PlanetTypes.drawType(pressure, temperatureForAlbedo, gravityPercent, + giant, CellHash.ofBody(seed, key, variant, SALT_TYPE)); + int temperature = temperatureForAlbedo.applyAsInt( + preset == null ? AstronomicalBodyHelper.EARTH_ALBEDO : preset.albedo()); TerrainOption terrain = PlanetTypes.drawTerrain(preset, CellHash.ofBody(seed, key, variant, SALT_TERRAIN)); @@ -371,8 +385,11 @@ public static BodyProfile deriveRogue(long seed, GalacticCoord bodyCell, int var int pressure = bulky ? DimensionProperties.MAX_ATM_PRESSURE : DimensionProperties.MIN_ATM_PRESSURE; int temperature = residualTemperature(mass, radius); - PlanetTypePreset preset = PlanetTypes.drawType(pressure, temperature, gravityPercent, bulky, - CellHash.ofBody(seed, key, variant, SALT_TYPE)); + // Albedo does not enter here, and that is a statement rather than a shortcut: albedo is the + // fraction of INCIDENT light a surface throws back, and nothing shines on this world. Its heat + // is its own, so every candidate type is admitted at the same temperature. + PlanetTypePreset preset = PlanetTypes.drawType(pressure, albedo -> temperature, gravityPercent, + bulky, CellHash.ofBody(seed, key, variant, SALT_TYPE)); TerrainOption terrain = PlanetTypes.drawTerrain(preset, CellHash.ofBody(seed, key, variant, SALT_TERRAIN)); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java index 04bea31ce..e06f108d8 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java @@ -203,7 +203,11 @@ private static DimensionProperties materialize(int dimId, BodyProfile profile, S props.orbitTheta = props.baseOrbitTheta; props.setAtmosphereDensityDirect(profile.pressure()); - props.averageTemperature = profile.temperatureKelvin(); + // STATED, never recomputed: the profile's number is the one a telescope already reported, and + // materialization is the moment it becomes the world's. The albedo is applied below, and after + // the derivation's second pass a recompute would reproduce this exact value anyway — which is + // the invariant, not a coincidence to lean on. + props.setAverageTemp(profile.temperatureKelvin()); props.hasOxygen = profile.hasOxygen(); props.setBulk(profile.massEarths(), profile.radiusEarths()); props.setTidallyLocked(profile.tidallyLocked()); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java index 326d9b2bd..27ebbbdc7 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetTypes.java @@ -3,11 +3,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.DoubleToIntFunction; import java.util.function.Predicate; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; + /** * The catalogue of {@link PlanetTypePreset planet types} and the two draws that use it: which type a * derived world IS, and which of that type's terrain generators it gets. @@ -100,12 +103,29 @@ public static void setWorldTypeAvailability(Predicate probe) { // ─── The draws ───────────────────────────────────────────────────────────── - /** Every preset whose declared region admits this world. May be empty (an authoring gap). */ - public static List candidates(int pressure, int temperatureKelvin, + /** + * Every preset whose declared region admits this world. May be empty (an authoring gap). + * + *

    Each candidate is tested at the temperature the world would have IF IT WERE THAT TYPE. + * A preset states its surface, a surface has an albedo, and the albedo is part of what sets the + * temperature — so admitting every candidate at one temperature and then applying the winner's + * albedo produced worlds outside their own declared band: an {@code ocean} preset admitting + * 255–380 K would be handed to a world that its own albedo of 0.10 then warms to 393 K.

    + * + *

    It is not circular and it does not iterate: the caller hands in a FUNCTION from albedo to + * temperature, so each candidate is evaluated once, against its own number. That also keeps the + * LAW out of this class — it stays a table matcher and never learns what a star is or how one + * warms a world.

    + * + * @param temperatureForAlbedo what this world's surface temperature would be at a given albedo + */ + public static List candidates(int pressure, + DoubleToIntFunction temperatureForAlbedo, int gravityPercent, boolean gasGiant) { List out = new ArrayList<>(); for (PlanetTypePreset p : presets) { - if (p.admits(pressure, temperatureKelvin, gravityPercent, gasGiant)) { + if (p.admits(pressure, temperatureForAlbedo.applyAsInt(p.albedo()), gravityPercent, + gasGiant)) { out.add(p); } } @@ -121,15 +141,21 @@ public static List candidates(int pressure, int temperatureKel * substituted and no preset is invented: the answer is {@code null}, and the caller reports the * world as {@link #UNCLASSIFIED}. A silent substitution would hide the authoring gap forever.

    */ - public static PlanetTypePreset drawType(int pressure, int temperatureKelvin, int gravityPercent, - boolean gasGiant, long hash) { - List admitting = candidates(pressure, temperatureKelvin, gravityPercent, gasGiant); + public static PlanetTypePreset drawType(int pressure, + DoubleToIntFunction temperatureForAlbedo, + int gravityPercent, boolean gasGiant, long hash) { + List admitting = candidates(pressure, temperatureForAlbedo, gravityPercent, + gasGiant); if (admitting.isEmpty()) { + // Reported at the NEUTRAL reading, which is the one number that describes the world rather + // than one of the types that declined it — an author widening a range needs to know where + // the world actually sits, not where the last candidate would have put it. + int neutral = temperatureForAlbedo.applyAsInt(AstronomicalBodyHelper.EARTH_ALBEDO); if (SystemContent.reportOnce("noPlanetType:" + gasGiant + ':' + pressure / 50 + ':' - + temperatureKelvin / 25 + ':' + gravityPercent / 25)) { + + neutral / 25 + ':' + gravityPercent / 25)) { LOGGER.warn("no planet type admits a world at pressure {}, {} K, gravity {}% (gasGiant={})" + " - it will be reported as '{}'. Widen a range to cover it.", - pressure, temperatureKelvin, gravityPercent, gasGiant, UNCLASSIFIED); + pressure, neutral, gravityPercent, gasGiant, UNCLASSIFIED); } return null; } diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index aa5f1be29..c0259490b 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -820,7 +820,7 @@ private static Node writePlanet(Document doc, DimensionProperties properties) { nodePlanet.appendChild(createTextNode(doc, ELEMENT_BASEORBITTHETA, Math.toDegrees(properties.baseOrbitTheta))); nodePlanet.appendChild(createTextNode(doc, ELEMENT_PHI, properties.orbitalPhi)); nodePlanet.appendChild(createTextNode(doc, ELEMENT_RETROGRADE, properties.isRetrograde)); - nodePlanet.appendChild(createTextNode(doc, AVG_TEMPERATURE, properties.averageTemperature)); + nodePlanet.appendChild(createTextNode(doc, AVG_TEMPERATURE, properties.getAverageTemp())); nodePlanet.appendChild(createTextNode(doc, ELEMENT_PERIOD, properties.rotationalPeriod)); nodePlanet.appendChild(createTextNode(doc, ELEMENT_ATMDENSITY, properties.getAtmosphereDensity())); // Custom weather properties @@ -1579,8 +1579,13 @@ else if (planetPropertyNode.getNodeName().equalsIgnoreCase(ELEMENT_RINGCOLOR)) { //Star may not be registered at this time, use ID version instead properties.setStar(star.getId()); - //Set temperature - properties.averageTemperature = AstronomicalBodyHelper.getAverageTemperature(star, properties.getSolarOrbitalDistance(), properties.getAtmosphereDensity()); + // Set temperature. From the LOCAL star object, not through properties.getStar(): the star is + // not in the catalogue yet (see the line above), so the lookup would come back null here and + // the world would be born at the temperature of deep space. The albedo is the world's own, so + // an authored planet and a derived one are warmed by the same law (ledger #289). + properties.setAverageTemp(AstronomicalBodyHelper.getAverageTemperature(star, + properties.getSolarOrbitalDistance(), properties.getAtmosphereDensity(), + properties.getAlbedo())); //If no biomes are specified add some! if (properties.getBiomes().isEmpty()) diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java index c62429627..eff04f999 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetDerivationTest.java @@ -76,6 +76,49 @@ private static List system(long seed, GalacticCoord anchor, Stellar return out; } + /** + * The scan and the landing describe the same world. + * + *

    {@code BodyProfile}'s own javadoc states this contract and nothing pinned it. A derived + * temperature is what a telescope reports from across the system; the realized dimension then + * recomputes one from the star, the orbit, the atmosphere and the world's ALBEDO — and the + * derivation used to end on the neutral-albedo overload, so the two disagreed by + * {@code ((1 − a)/0.7)^¼} for every world whose type states an albedo of its own. Measured on the + * shipped table: a {@code greenhouse} world (a = 0.75) landed 22.7 % colder than it scanned and an + * {@code ice} world 13 % (ledger #289).

    + * + *

    What this pins is not the second pass but the AGREEMENT: whatever law either side uses, the + * number a profile carries has to be the number the dimension model produces from that profile's + * own inputs. It is asserted exactly, because "the same world" admits no tolerance.

    + */ + @Test + public void theTemperatureAScanReportsIsTheTemperatureTheWorldHas() { + int compared = 0; + Set albedosSeen = new HashSet<>(); + for (long c = 0; c < 400; c++) { + GalacticCoord anchor = cell(9000 + c, 0, 0); + StellarBody s = c % 2 == 0 ? sol() : star(45, 0.7f); + for (BodyProfile profile : system(SEED + c, anchor, s, 6)) { + double albedo = profile.preset() == null + ? zmaster587.advancedRocketry.util.AstronomicalBodyHelper.EARTH_ALBEDO + : profile.preset().albedo(); + albedosSeen.add(Double.toString(albedo)); + // Exactly the call DimensionProperties.recalculateTemperature makes on a world + // materialized from this profile: its star, its orbit, its air, its own albedo. + int asTheWorldWillReadIt = + zmaster587.advancedRocketry.util.AstronomicalBodyHelper.getAverageTemperature( + s, Math.max(1, profile.orbitalDistance()), profile.pressure(), albedo); + assertEquals("a " + profile.typeName() + " world (albedo " + albedo + ") scanned at " + + profile.temperatureKelvin() + " K must not land at another temperature", + profile.temperatureKelvin(), asTheWorldWillReadIt); + compared++; + } + } + assertTrue("the sweep must actually derive worlds", compared > 100); + assertTrue("and it must cross types whose albedo is NOT Earth's, or it proves nothing about " + + "the defect it exists for - saw " + albedosSeen, albedosSeen.size() > 2); + } + /** * A world's DAY is drawn, and it is not a function of its gravity. * @@ -527,7 +570,7 @@ public void overlappingPresetsShareTheirProbabilityByWeight() { Map counts = new HashMap<>(); for (int i = 0; i < 5000; i++) { - PlanetTypePreset p = PlanetTypes.drawType(100, 280, 100, false, + PlanetTypePreset p = PlanetTypes.drawType(100, albedo -> 280, 100, false, i * 0x9E3779B97F4A7C15L); counts.merge(p.name(), 1, Integer::sum); } @@ -545,7 +588,7 @@ public void aWorldNoPresetAdmitsIsReportedRatherThanSubstituted() { .pressure(0, 10).temperature(0, 10).gravity(0, 10).build()); PlanetTypes.setPresets(table); assertEquals("silently substituting a preset would hide the coverage gap for ever", - null, PlanetTypes.drawType(900, 900, 300, false, 1L)); + null, PlanetTypes.drawType(900, albedo -> 900, 300, false, 1L)); } /** A star archetype that varies across the sweep, so no test measures one kind of system only. */ From 02e5cc922b225e35eb215c150f15a87e893626d7 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Tue, 18 Aug 2026 19:26:03 +0300 Subject: [PATCH 37/42] feat: the void's numbers are measured, not chosen - take the observed free-floating abundance and giant fraction - expose rogue tuning through galaxyGen, read and written back - give a rogue its own giant rate instead of the bound outer-zone one - move the feed fixture clear of the shipped system's neighbourhood --- .../universe/ClusteredGalaxyGenerator.java | 15 +- .../advancedRocketry/universe/Galaxy.java | 28 ++-- .../universe/GalaxyField.java | 5 +- .../universe/GalaxyGenConfig.java | 139 ++++++++++++++---- .../universe/PlanetDerivation.java | 14 +- .../util/XMLPlanetLoader.java | 18 ++- ...SystemBodiesFeedFollowsTheCellE2ETest.java | 26 +++- .../unit/ClusteredGalaxyGeneratorTest.java | 20 ++- .../test/unit/VoidContentTest.java | 4 +- 9 files changed, 202 insertions(+), 67 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index c4871e2c6..edaeca2b4 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -205,7 +205,7 @@ public ClusteredGalaxyGenerator(GalaxyGenConfig config) { w += t.weight; } this.totalStarWeight = Math.max(1L, w); - this.rogueTypes = GalaxyGenConfig.defaultRogueTypes(); + this.rogueTypes = this.config.rogue.types; long rw = 0L; for (GalaxyGenConfig.RogueType t : this.rogueTypes) { rw += t.weight; @@ -331,7 +331,7 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { // A system whose primary is not a star: no companions, no zone, no orbits — the whole // second half of the retinue law is about distances FROM a star. What it can still have is // moons, so that is what it gets. - return rogueBodiesFor(seed, cell, systemId); + return rogueBodiesFor(seed, cell, systemId, config.rogue.giantFraction); } StellarBody star = sys.get().star().get(); List bodies = new ArrayList<>(); @@ -399,9 +399,10 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { * inner few and nothing else — the same ceiling a rocky world has, applied whatever its bulk, * rather than the ceiling its mass would otherwise buy it.

    */ - private static List rogueBodiesFor(long seed, GalacticCoord cell, int systemId) { + private static List rogueBodiesFor(long seed, GalacticCoord cell, int systemId, + double giantFraction) { List bodies = new ArrayList<>(); - BodyProfile profile = PlanetDerivation.deriveRogue(seed, cell, 0); + BodyProfile profile = PlanetDerivation.deriveRogue(seed, cell, 0, giantFraction); // It does not move inside its own system: it IS the system, so its frame is the anchor's. bodies.add(SystemBody.fixedAt(cell, SystemBodyKind.ROGUE_PLANET, Constants.INVALID_PLANET, systemId).withRadius(profile.radiusEarths())); @@ -424,7 +425,7 @@ private static List rogueBodiesFor(long seed, GalacticCoord cell, in SystemContent.MOON_UNIT_BLOCKS); // A moon of a rogue is starless too, so it is derived the same way its parent was, one // variant along — never through the star-lit law with a star that is not there. - BodyProfile moonProfile = PlanetDerivation.deriveRogue(seed, cell, j); + BodyProfile moonProfile = PlanetDerivation.deriveRogue(seed, cell, j, giantFraction); bodies.add(new SystemBody(cell, frame, law, SystemBodyKind.MOON, Constants.INVALID_PLANET, systemId, SystemBody.ORBIT_UNKNOWN) .withRadius(moonProfile.radiusEarths())); @@ -699,7 +700,7 @@ public BodyProfile profileOf(long seed, GalacticCoord anchor, SystemBody body, S // Nothing lights this system, so nothing about the body follows from a distance: it is the // starless derivation or it is a body whose physics would be read off a star that is not // there. A moon of a rogue takes the same branch, which is right — it is starless too. - return PlanetDerivation.deriveRogue(seed, body.name(), variant); + return PlanetDerivation.deriveRogue(seed, body.name(), variant, config.rogue.giantFraction); } return PlanetDerivation.derive(seed, anchor.cellCentre(), body.name(), variant, star, body.kind() == SystemBodyKind.MOON, body.orbitalDistance()); @@ -868,7 +869,7 @@ private Optional rogueForLattice(long seed, Lattice lattice, double p if (!(profile > 0d)) { return Optional.empty(); // a galaxy cell with no galaxy in it: the deepest void, and empty } - double occupancy = Math.min(1d, config.density * GalaxyGenConfig.ROGUE_ABUNDANCE * profile); + double occupancy = Math.min(1d, config.density * config.rogue.abundance * profile); if (CellHash.norm(lattice.hash(seed, SALT_ROGUE_OCC)) >= occupancy) { return Optional.empty(); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java index 4d2be1481..94564a46d 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java @@ -65,16 +65,6 @@ public final class Galaxy { */ public static final double EDGE_LEVEL = Math.exp(-1d / DISC_SCALE_FRACTION) / REFERENCE_LEVEL; - /** - * How steeply a galaxy's ejecta thins outside it, as a power of the distance in radii. - * - *

    Three, because that is what a population thrown out over a Hubble time and spread through a - * growing volume comes to — the same slope the outer parts of a real stellar halo and the - * intracluster light are measured at. It is not the disc's exponential: an exponential in units of - * the radius is dead within a few of them, and the void is twenty-five across.

    - */ - private static final double EJECTA_FALLOFF = 3d; - private final long cellX; private final long cellY; private final long cellZ; @@ -313,26 +303,32 @@ public double densityAtSector(long sectorX, long sectorY, long sectorZ) { * ejects nothing into itself: inside its own sphere the bound profile is what says how much * material is at a point, and adding a second term there would double-count the same stars.

    * - *

    Outside, it falls as {@code (R/r)³} from {@link #EDGE_LEVEL} — anchored at the edge, so a big + *

    Outside, it falls as {@code (R/r)^falloff} from {@link #EDGE_LEVEL} — anchored at the edge, so a big * galaxy fills far more of the void than a dwarf and neither needs a normalisation of its own. It * is ISOTROPIC while the disc is not: ejection randomises a direction long before a body has * crossed the void, so a spiral's poles are not a dead cone. The step at the radius is therefore * real, and it is at the one surface this layer already declares as a boundary — the surface where * the frame flips and where the star field stops dead.

    */ - public double ejectaDensityAt(double dxLy, double dyLy, double dzLy) { + public double ejectaDensityAt(double dxLy, double dyLy, double dzLy, double falloff) { double r = Math.sqrt(dxLy * dxLy + dyLy * dyLy + dzLy * dzLy); if (r <= radiusLy) { return 0d; } - return EDGE_LEVEL * Math.pow(radiusLy / r, EJECTA_FALLOFF); + return EDGE_LEVEL * Math.pow(radiusLy / r, falloff); } - /** The ejecta halo read at a cell name — the form the generator asks in. */ - public double ejectaDensityAtSector(long sectorX, long sectorY, long sectorZ) { + /** + * The ejecta halo read at a cell name — the form the generator asks in. + * + *

    The exponent is the CALLER's, out of {@code GalaxyGenConfig.RogueTuning}: a galaxy is a value + * drawn from a hash and knows nothing about how the universe is tuned, and giving it a config + * would make two galaxies of one seed differ by which config happened to draw them.

    + */ + public double ejectaDensityAtSector(long sectorX, long sectorY, long sectorZ, double falloff) { return ejectaDensityAt(offsetLy(sectorX, centre.sectorX()), offsetLy(sectorY, centre.sectorY()), - offsetLy(sectorZ, centre.sectorZ())); + offsetLy(sectorZ, centre.sectorZ()), falloff); } /** diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java index 479fcd30c..275cf0bcc 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java @@ -273,7 +273,8 @@ public Material materialAtSector(long seed, long sectorX, long sectorY, long sec // and this is the hottest path in the layer, taken for every cell of the shipped galaxy. return new Material(primary.densityAtSector(sectorX, sectorY, sectorZ), 0d); } - double unbound = primary.ejectaDensityAtSector(sectorX, sectorY, sectorZ); + double falloff = config.rogue.ejectaFalloff; + double unbound = primary.ejectaDensityAtSector(sectorX, sectorY, sectorZ, falloff); if (!withinRetinueReach(primary, sectorX, sectorY, sectorZ)) { return new Material(0d, unbound); // past the retinue: only the primary's own halo reaches } @@ -285,7 +286,7 @@ public Material materialAtSector(long seed, long sectorX, long sectorY, long sec // material counted twice, and adding them would make the gap between two dwarfs read // denser than either dwarf's own edge. unbound = Math.max(unbound, - satellite.ejectaDensityAtSector(sectorX, sectorY, sectorZ)); + satellite.ejectaDensityAtSector(sectorX, sectorY, sectorZ, falloff)); } return new Material(0d, unbound); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index 6fdb1e86e..c8a4247b7 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -218,6 +218,12 @@ public ClusterType(String name, int subdivision, double minRadiusLy, double maxR * against. Always contains {@link GalaxyKey#HOME}: a pack that names no galaxy still has one. */ public final List reservedGalaxies; + /** + * What the UNBOUND population looks like — how many free-floating worlds there are, what they are + * made of, and how far a galaxy's ejecta reaches. Never {@code null}; defaults to + * {@link RogueTuning#physical()}, i.e. to what is measured. + */ + public final RogueTuning rogue; /** * Each lattice states its EDGE and then its OCCUPANCY, stars first and galaxies second, so the two @@ -252,6 +258,31 @@ public GalaxyGenConfig(int minSpacing, double density, long galaxySpacing, doubl } } this.reservedGalaxies = Collections.unmodifiableList(reserved); + this.rogue = RogueTuning.physical(); + } + + private GalaxyGenConfig(GalaxyGenConfig from, RogueTuning rogue) { + this.density = from.density; + this.minSpacing = from.minSpacing; + this.galaxySpacing = from.galaxySpacing; + this.galaxyDensity = from.galaxyDensity; + this.starTypes = from.starTypes; + this.galaxyTypes = from.galaxyTypes; + this.clusterTypes = from.clusterTypes; + this.reservedGalaxies = from.reservedGalaxies; + this.rogue = rogue == null ? RogueTuning.physical() : rogue; + } + + /** + * The same configuration with the unbound population retuned — the {@code } attributes + * a pack may state about rogues. + * + *

    A named copy rather than four more constructor parameters, and the same shape + * {@link #withReservedGalaxies} already uses: what ships is the measured universe, and a pack + * states only the part it disagrees with.

    + */ + public GalaxyGenConfig withRogueTuning(RogueTuning tuning) { + return new GalaxyGenConfig(this, tuning); } /** @@ -368,17 +399,82 @@ private static List defaultClusterTypes() { /** Fraction of those cubes that hold a cluster, before the galaxy's own profile scales it. */ public static final double CLUSTER_DENSITY = 0.35d; - // ─── The unbound population ──────────────────────────────────────────────── - // What a lattice cube holds when no star was seated in it. Stated as constants beside the cluster - // tier's and for the same reason: it is a whole tier's worth of numbers, none of them yet ratified, - // and neither tier is authorable from today. + /** + * The unbound population's tuning: how many free-floating worlds there are, what they are made of, + * and how far a galaxy's ejecta reaches. + * + *

    Every default here is a MEASURED astronomical quantity rather than a balance choice, because + * the rest of this layer already is — the star separation, the galaxy radii and the galaxy + * separation are all real. A pack that wants a different sky changes them through + * {@code }; what ships states what is out there.

    + */ + public static final class RogueTuning { + + /** + * How many unbound worlds the lattice draws for each STAR, at the same point. + * + *

    21, and it is an observation. Nine years of MOA-II microlensing put the + * terrestrial-mass free-floating population at roughly twenty per main-sequence star, and the + * worlds this generator draws are overwhelmingly rocky, so that is the matching number. The + * older headline of ~1.8 Jupiter-mass objects per star was retracted by OGLE, which caps that + * mass range at ~0.25 — see {@link #giantFraction}.

    + * + *

    The lattice SATURATES this, and the saturation is the honest reading rather than a + * bug. A cube holds at most one seat, so any abundance past {@code 1/density} means "every + * territory the stars left empty has something in it", which is exactly what twenty per star + * says when a territory is one star's worth of space. Lowering it below that threshold is what + * makes the number visible again.

    + */ + public final double abundance; + + /** + * The fraction of unbound worlds massive enough to have kept hydrogen — a giant rather than + * a rock. + * + *

    Far below the ordinary outer-zone giant chance, and for a physical reason: what + * unbinds a planet is a scattering encounter, and a giant is the body doing the scattering + * rather than the one thrown out. The number is the ratio of the two measured populations — + * ~0.25 Jupiter-mass free floaters per star against ~21 terrestrial ones — so about one in + * eighty. Inheriting the 0.34 that a bound body past the snow line gets would have produced + * half a free-floating giant per star, two orders above what is seen.

    + */ + public final double giantFraction; + + /** + * How steeply a galaxy's ejecta thins outside it, as a power of the distance in radii. + * + *

    Three: the slope the outer parts of a stellar halo and the intracluster light are + * measured at, which is what a population thrown out over a Hubble time into a growing volume + * comes to. Not the disc's exponential — an exponential in units of the radius is dead within + * a few of them, and the void is twenty-five across.

    + */ + public final double ejectaFalloff; + + /** What an unbound seat turns out to hold, by weight (never empty). */ + public final List types; + + public RogueTuning(double abundance, double giantFraction, double ejectaFalloff, + List types) { + this.abundance = (Double.isNaN(abundance) || abundance < 0d) ? 0d : abundance; + this.giantFraction = clamp01(giantFraction); + this.ejectaFalloff = (Double.isNaN(ejectaFalloff) || ejectaFalloff <= 0d) + ? 3d : ejectaFalloff; + this.types = (types == null || types.isEmpty()) + ? defaultRogueTypes() : Collections.unmodifiableList(new ArrayList<>(types)); + } + + /** The measured universe: what the sky actually holds. */ + public static RogueTuning physical() { + return new RogueTuning(21d, 0.012d, 3d, defaultRogueTypes()); + } + } /** * A weighted ROGUE archetype — what an unbound seat turns out to hold. The fourth table of the * shape {@link StarType} / {@link GalaxyType} / {@link ClusterType} use, and it exists for the * same reason they do: relative abundance is a WEIGHT, so "by falling abundance" is a * property of the table rather than a rule somewhere in the generator, and adding a kind of - * unbound object later is one row instead of a fourth occupancy knob. + * unbound object later is one row instead of another occupancy knob. */ public static final class RogueType { public final String name; @@ -394,36 +490,23 @@ public RogueType(String name, SystemBodyKind primaryKind, int weight) { } /** - * How many unbound seats the lattice draws for each STAR it draws, at the same point. + * The stock rogue table, and the ratio in it is measured too. * - *

    It multiplies the same {@link #density} against the same profile, which is what makes it an - * occupancy FACTOR rather than a density of its own: everything already built — the super-cell - * partition, member-cell attribution, the survey's stride, the seat margins — keeps working - * untouched, and "more numerous than stars" is one number.

    - * - *

    Above one because free-floating worlds really do outnumber stars; at the LOW end of the - * observed band, which runs from comparable to some tens of times, because a lattice cube is a - * STAR's territory and seating a rogue in one claims rogues partition space the way stars do. - * Measured at the stock density: in a sun-like neighbourhood this seats about as many rogue - * worlds as stars, because a cube the star draw already took is not offered twice.

    - */ - public static final double ROGUE_ABUNDANCE = 1.5d; - - /** - * The stock rogue table. A thrown-out WORLD is the ordinary case and a thrown-out STAR is the - * find: ejecting a star takes an encounter violent enough to unbind the heaviest thing in a - * system, while a planet is unbound by the ordinary jostling of the system it formed in. + *

    A thrown-out WORLD against a thrown-out STAR is ~21 per star against the few per cent of + * stars that end up unbound from their galaxy at all — the intragroup population a galaxy group + * carries, well below the intracluster fractions a rich cluster shows. So a rogue star is about + * one seat in a thousand, which is what makes meeting a whole lit system out in the void an event + * rather than routine.

    * *

    A rogue star is a {@link SystemBodyKind#STAR} and nothing else — rogue-ness is a statement - * about WHERE it stands, not about what it is, so it is seated as an ordinary system and gets an - * ordinary retinue. That is why finding one out here is an event: it is a whole system, in a place - * where a system has no business being.

    + * about WHERE it stands, not about what it is — so it is fabricated by the ordinary path and gets + * an ordinary retinue.

    */ public static List defaultRogueTypes() { List l = new ArrayList<>(); // name what is seated weight - l.add(new RogueType("Rogue Planet", SystemBodyKind.ROGUE_PLANET, 200)); - l.add(new RogueType("Rogue Star", SystemBodyKind.STAR, 3)); + l.add(new RogueType("Rogue Planet", SystemBodyKind.ROGUE_PLANET, 1050)); + l.add(new RogueType("Rogue Star", SystemBodyKind.STAR, 1)); return Collections.unmodifiableList(l); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java index 82388c6ce..6e7546cb4 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetDerivation.java @@ -369,15 +369,21 @@ public static BodyProfile derive(long seed, GalacticCoord anchor, GalacticCoord * is not realized into a dimension yet. Its bulk is in the profile for anything that wants it.

    * * @param variant disambiguates bodies SHARING a cell — the rogue itself is 0 and its moons follow + * @param giantFraction how many unbound worlds kept hydrogen; see + * {@code GalaxyGenConfig.RogueTuning.giantFraction}. It is NOT the outer-zone + * chance a bound body past the snow line gets — what unbinds a planet is a + * scattering encounter, and a giant is the body doing the scattering */ - public static BodyProfile deriveRogue(long seed, GalacticCoord bodyCell, int variant) { + public static BodyProfile deriveRogue(long seed, GalacticCoord bodyCell, int variant, + double giantFraction) { GalacticCoord key = bodyCell.cellCentre(); // Its own draw, because it has no star to have inherited one from. A rogue formed in some // system and carries that system's metals; which system is not a thing this layer can know. double metallicity = metallicityOf(seed, key); - // Colder than any snow line, by construction — so the giant roll is the outer-zone one, which - // is the same law every other body past the frost line is drawn by rather than a rate of its own. - boolean bulky = isGiantAt(seed, key, variant, 0); + // Its OWN rate, and the difference from a bound body's is the physics: a world past the frost + // line accretes a giant about a third of the time, while a world thrown out of its system is + // overwhelmingly one of the light ones — the giant is what did the throwing. + boolean bulky = CellHash.norm(CellHash.ofBody(seed, key, variant, SALT_GIANT)) < giantFraction; double radius = radiusOf(seed, key, variant, bulky, false); double mass = massOf(seed, key, variant, radius, bulky); diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index c0259490b..73937f0dd 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -98,6 +98,9 @@ public class XMLPlanetLoader { private static final String ATTR_MINSPACING = "minSpacing"; private static final String ATTR_GALAXYSPACING = "galaxySpacing"; private static final String ATTR_GALAXYDENSITY = "galaxyDensity"; + private static final String ATTR_ROGUEABUNDANCE = "rogueAbundance"; + private static final String ATTR_ROGUEGIANTFRACTION = "rogueGiantFraction"; + private static final String ATTR_EJECTAFALLOFF = "ejectaFalloff"; private static final String ATTR_MINSIZE = "minSize"; private static final String ATTR_MAXSIZE = "maxSize"; private static final String ELEMENT_PLANET = "planet"; @@ -322,9 +325,17 @@ private GalaxyGenConfig readGalaxyGen(Node node) { galaxyTypes.add(readGalaxyType(child)); } } + // The UNBOUND population. Its defaults are measured quantities rather than balance picks, so + // an element that says nothing about rogues gets the sky as it is observed to be. + GalaxyGenConfig.RogueTuning rogueDefaults = defaults.rogue; + GalaxyGenConfig.RogueTuning rogue = new GalaxyGenConfig.RogueTuning( + attrDouble(node, ATTR_ROGUEABUNDANCE, rogueDefaults.abundance), + attrDouble(node, ATTR_ROGUEGIANTFRACTION, rogueDefaults.giantFraction), + attrDouble(node, ATTR_EJECTAFALLOFF, rogueDefaults.ejectaFalloff), + rogueDefaults.types); // Empty / lists fall back to the stock archetypes (config ctor). return new GalaxyGenConfig(minSpacing, density, galaxySpacing, galaxyDensity, types, - galaxyTypes); + galaxyTypes).withRogueTuning(rogue); } /** @@ -578,6 +589,11 @@ private static Element writeGalaxyGen(Document doc, GalaxyGenConfig cfg) { e.setAttribute(ATTR_MINSPACING, Integer.toString(cfg.minSpacing)); e.setAttribute(ATTR_GALAXYSPACING, Long.toString(cfg.galaxySpacing)); e.setAttribute(ATTR_GALAXYDENSITY, Double.toString(cfg.galaxyDensity)); + // Written back for the same reason the tables are: this file is REWRITTEN on every world save, + // so anything the reader did not turn into model state is silently lost on the first one. + e.setAttribute(ATTR_ROGUEABUNDANCE, Double.toString(cfg.rogue.abundance)); + e.setAttribute(ATTR_ROGUEGIANTFRACTION, Double.toString(cfg.rogue.giantFraction)); + e.setAttribute(ATTR_EJECTAFALLOFF, Double.toString(cfg.rogue.ejectaFalloff)); for (GalaxyGenConfig.StarType t : cfg.starTypes) { Element st = doc.createElement(ELEMENT_STARTYPE); st.setAttribute(ATTR_TEMP, Integer.toString(t.temperature)); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/SystemBodiesFeedFollowsTheCellE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/SystemBodiesFeedFollowsTheCellE2ETest.java index 86a6d09ca..b68b2b453 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/SystemBodiesFeedFollowsTheCellE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/SystemBodiesFeedFollowsTheCellE2ETest.java @@ -3,6 +3,8 @@ import org.junit.After; import org.junit.Test; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; + import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,12 +41,26 @@ public class SystemBodiesFeedFollowsTheCellE2ETest extends AbstractSharedServerTest { /** - * Cells are chosen with a non-zero sector Y, which keeps them clear of the generated fallback stars - * (all at {@code sy=sz=0}) — so the body count of a cell is exactly what this test put in it. The two - * methods use different cells: the shared server runs both, and a cell is global state. + * Cells far enough out that nothing else claims them, so the body count of a cell is exactly what + * this test put in it. The two methods use different cells: the shared server runs both, and a + * cell is global state. + * + *

    The distance that matters is {@code minSpacing/2}, not "away from sector zero". These + * used to sit at {@code sy = 5000} on the reasoning that a non-zero sector Y kept them clear of the + * generated fallback stars at {@code sy=sz=0} — which guarded against the wrong neighbour and left + * both legs failing. A cell is attributed to a stored anchor by + * {@code UniverseRegistry.storedAnchorNear}, whose reach is HALF THE SUPER-CELL — about 2 501 180 + * cells at the shipped spacing — so {@code sy = 5000} is 0.2 % of the way out and both cells were + * squarely inside the shipped solar system's own neighbourhood. The feed was answering correctly: + * it offered the sun, the overworld and a moon at 1.6·10¹¹ blocks (ledger #291).

    + * + *

    Stated as a multiple of the reach rather than as a literal, so the fixture cannot silently + * move back inside the neighbourhood the day the spacing is retuned.

    */ - private static final String CELL_NO_SHIP = "31 5000 2"; - private static final String CELL_MID_JUMP = "32 5000 2"; + private static final long CLEAR_OF_ANY_ANCHOR = + 3L * (GalaxyGenConfig.DEFAULT_MIN_SPACING / 2L); + private static final String CELL_NO_SHIP = "31 " + CLEAR_OF_ANY_ANCHOR + " 2"; + private static final String CELL_MID_JUMP = "32 " + CLEAR_OF_ANY_ANCHOR + " 2"; /** A body a few thousand blocks out, i.e. the geometry a pilot has to fly at to descend. */ private static final String BODY_LOCAL = "2900 0 -1200"; diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index 764181f9f..c5ba5e9b1 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -576,6 +576,15 @@ private static List anchors(ClusteredGalaxyGenerator gen, long se * than a single super-cell is what makes the count a reading of the density there instead of one * coin toss. */ + /** + * How many STAR seats a block of super-cells holds — never how many seats of any kind. + * + *

    The difference is load-bearing at the shipped tuning. Free-floating worlds are drawn on the + * same lattice at a MEASURED twenty-one per star, which saturates it: past {@code 1/density} every + * cube the star draw passed over holds something, so a count of occupied seats is the constant + * "all of them" and discriminates neither the density nor the galaxy profile. Both of the tests + * below exist to show that those two DO drive the star field, so both must count stars.

    + */ private static int seatsInBlockAround(ClusteredGalaxyGenerator gen, long offsetCells, long r) { Set seen = new HashSet<>(); for (long x = -r; x <= r; x++) { @@ -583,7 +592,7 @@ private static int seatsInBlockAround(ClusteredGalaxyGenerator gen, long offsetC for (long z = -r; z <= r; z++) { Optional a = gen.anchorAt(SEED, cell(offsetCells + x * SPACING, y * SPACING, z * SPACING)); - if (a.isPresent()) { + if (a.isPresent() && gen.systemAt(SEED, a.get()).get().star().isPresent()) { seen.add(a.get().cellKey()); } } @@ -592,11 +601,18 @@ private static int seatsInBlockAround(ClusteredGalaxyGenerator gen, long offsetC return seen.size(); } + /** + * The STAR seats of a sweep, by cell key — see {@link #seatsInBlockAround} for why it is stars and + * not seats of any kind: the unbound draw saturates the lattice at the shipped tuning, so a count + * of everything is the constant "every cube" and measures nothing. + */ private static Set occupiedSeats(ClusteredGalaxyGenerator gen, long seed, long spacing, long r) { Set keys = new HashSet<>(); for (GalacticCoord a : anchors(gen, seed, spacing, r)) { - keys.add(a.cellKey()); + if (gen.systemAt(seed, a).get().star().isPresent()) { + keys.add(a.cellKey()); + } } return keys; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/VoidContentTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/VoidContentTest.java index 97da89de7..09f9f802b 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/VoidContentTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/VoidContentTest.java @@ -238,7 +238,7 @@ public void aRogueIsWarmedByItselfAndByNothingElse() { // star, rather than a gap where the insolation used to be. ClusteredGalaxyGenerator gen = gen(); GalacticCoord anchor = aRogueAnchor(gen); - BodyProfile profile = PlanetDerivation.deriveRogue(SEED, anchor, 0); + BodyProfile profile = PlanetDerivation.deriveRogue(SEED, anchor, 0, GalaxyGenConfig.RogueTuning.physical().giantFraction); assertEquals(SystemBodyKind.ROGUE_PLANET, profile.kind()); assertTrue("a starless world is colder than anything a star lights: " + profile.temperatureKelvin() @@ -250,7 +250,7 @@ public void aRogueIsWarmedByItselfAndByNothingElse() { assertEquals("and no orbit of its own", SystemBody.ORBIT_UNKNOWN, profile.orbitalDistance()); assertEquals("deterministic, like every other derived body", profile.temperatureKelvin(), - PlanetDerivation.deriveRogue(SEED, anchor, 0).temperatureKelvin()); + PlanetDerivation.deriveRogue(SEED, anchor, 0, GalaxyGenConfig.RogueTuning.physical().giantFraction).temperatureKelvin()); } @Test From 5bcc26b9e17b3b95509bbfe2e8f4bb435bf8f69c Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 19 Aug 2026 11:11:46 +0300 Subject: [PATCH 38/42] feat: version the universe schema, not the mod - schema selects generator, derivation, metric and expansion - every released version stays in the jar; the save picks its own - save stamps schema version, config and laws fingerprints - a telescope look pins the system it reports - golden corpus of 7 seeds decides minor vs major byte for byte - ships as version 0 "0.1", warned as alpha at boot and on login - /stellurgy universe status and upgrade, with one-shot arming --- build.gradle | 8 + .../advancedRocketry/AdvancedRocketry.java | 12 + .../command/ARCommandRoot.java | 3 + .../sub/planet/PlanetGenerateCommand.java | 6 +- .../command/sub/universe/UniverseCommand.java | 28 + .../sub/universe/UniverseStatusCommand.java | 76 + .../sub/universe/UniverseUpgradeCommand.java | 132 ++ .../command/sub/universe/package-info.java | 5 + .../command/test/TestProbeCommand.java | 3 +- .../dimension/DimensionManager.java | 18 +- .../space/SkyNebulaeProducer.java | 9 +- .../universe/BodyDerivationV0.java | 79 + .../advancedRocketry/universe/CellHash.java | 15 +- .../universe/ClusterField.java | 11 +- .../universe/ClusteredGalaxyGenerator.java | 75 +- .../universe/Fingerprint.java | 46 + .../universe/GalacticAnchor.java | 7 +- .../advancedRocketry/universe/Galaxy.java | 16 +- .../universe/GalaxyField.java | 43 +- .../universe/GalaxyGenConfig.java | 76 +- .../universe/IBodyDerivation.java | 58 + .../universe/IGalaxyGenerator.java | 39 + .../universe/IUniverseLaws.java | 58 + .../universe/LightYearVector.java | 14 +- .../advancedRocketry/universe/Nebula.java | 11 +- .../universe/NebulaField.java | 33 +- .../universe/PlanetRealizer.java | 3 +- .../advancedRocketry/universe/RegionScan.java | 8 +- .../universe/TelescopeScan.java | 13 + .../universe/UniverseLawsV0.java | 71 + .../universe/UniverseRegistry.java | 312 +++- .../universe/UniverseSchema.java | 99 ++ .../UniverseSchemaMismatchException.java | 22 + .../universe/UniverseSchemaV0.java | 44 + .../universe/UniverseSchemas.java | 75 + .../util/XMLPlanetLoader.java | 6 +- .../assets/advancedrocketry/lang/en_US.lang | 21 + .../unit/ClusteredGalaxyGeneratorTest.java | 389 +++++ .../test/unit/GalaxyFieldTest.java | 19 +- .../test/unit/GalaxyTest.java | 11 +- .../test/unit/SkyNebulaeProducerTest.java | 5 +- .../test/unit/StarClusterTest.java | 3 +- .../test/unit/TelescopeRegionScanTest.java | 110 ++ .../test/unit/UniverseRegistryTest.java | 407 +++++ .../resources/universe/golden-corpus-v1.txt | 1511 +++++++++++++++++ 45 files changed, 3893 insertions(+), 117 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseCommand.java create mode 100644 src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseStatusCommand.java create mode 100644 src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseUpgradeCommand.java create mode 100644 src/main/java/zmaster587/advancedRocketry/command/sub/universe/package-info.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/BodyDerivationV0.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/Fingerprint.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/IBodyDerivation.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/IUniverseLaws.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/UniverseLawsV0.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/UniverseSchema.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemaMismatchException.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemaV0.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemas.java create mode 100644 src/test/resources/universe/golden-corpus-v1.txt diff --git a/build.gradle b/build.gradle index b42611d9a..0abe039fe 100644 --- a/build.gradle +++ b/build.gradle @@ -315,6 +315,14 @@ def configureHeadlessTest = { Test t, String packageGlob -> }) // Test-only flag gating /artest probe commands and other test-only behaviour. t.systemProperty 'advancedrocketry.tests', 'true' + // Forward the golden-corpus rewrite flag into the forked test JVM. Regenerating the universe + // fixture is a deliberate act (it means a new schema version is being released), so it is opt-in + // per invocation rather than a property with a default: + // ./gradlew testUnit --rerun -Dadvancedrocketry.universe.corpus.write=true + if (System.getProperty('advancedrocketry.universe.corpus.write') != null) { + t.systemProperty 'advancedrocketry.universe.corpus.write', + System.getProperty('advancedrocketry.universe.corpus.write') + } t.testLogging { events 'failed', 'skipped', 'passed' exceptionFormat = 'full' diff --git a/src/main/java/zmaster587/advancedRocketry/AdvancedRocketry.java b/src/main/java/zmaster587/advancedRocketry/AdvancedRocketry.java index c6728d3bb..34a37fc6d 100644 --- a/src/main/java/zmaster587/advancedRocketry/AdvancedRocketry.java +++ b/src/main/java/zmaster587/advancedRocketry/AdvancedRocketry.java @@ -1441,6 +1441,18 @@ public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { PacketHandler.sendToPlayer(new PacketSyncKnownPlanets(station.getId(), station.getKnownPlanetList()), player); } } + + // An ALPHA world model is told to the player, on the world it applies to, every time he + // arrives. Not once and not in a changelog: what it warns about is that this world may have + // no way forward, and that is worth knowing before he invests another evening in it. + zmaster587.advancedRocketry.universe.UniverseRegistry.activeSchema().ifPresent(schema -> { + if (!schema.isStable()) { + player.sendMessage(new net.minecraft.util.text.TextComponentTranslation( + "msg.advancedrocketry.universe.alpha", schema.label()) + .setStyle(new net.minecraft.util.text.Style() + .setColor(net.minecraft.util.text.TextFormatting.GOLD))); + } + }); } } } diff --git a/src/main/java/zmaster587/advancedRocketry/command/ARCommandRoot.java b/src/main/java/zmaster587/advancedRocketry/command/ARCommandRoot.java index e1e3aa3c9..d37798df9 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/ARCommandRoot.java +++ b/src/main/java/zmaster587/advancedRocketry/command/ARCommandRoot.java @@ -15,6 +15,7 @@ import zmaster587.advancedRocketry.command.sub.station.StationCommand; import zmaster587.advancedRocketry.command.sub.teleport.FetchCommand; import zmaster587.advancedRocketry.command.sub.teleport.GoToCommand; +import zmaster587.advancedRocketry.command.sub.universe.UniverseCommand; import javax.annotation.Nullable; import java.util.ArrayList; @@ -28,6 +29,7 @@ public ARCommandRoot() { aliases.add("advancedrocketry"); aliases.add("advrocketry"); aliases.add("ar"); + aliases.add("stellurgy"); addSubcommand(new WeatherCommand()); addSubcommand(new AddSealantCommand()); @@ -40,6 +42,7 @@ public ARCommandRoot() { addSubcommand(new StationCommand()); addSubcommand(new GoToCommand()); addSubcommand(new FillDataCommand()); + addSubcommand(new UniverseCommand()); addSubcommand(new DevCommand()); addSubcommand(new CommandTreeHelp(this)); diff --git a/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java b/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java index f242bbc42..bd4d2218c 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/sub/planet/PlanetGenerateCommand.java @@ -86,9 +86,11 @@ public void execute(MinecraftServer server, ICommandSender sender, String[] args // world rather than the same one again — and the sequence is reproducible on a fresh world. int index = star.getNumPlanets(); GalacticCoord anchor = GalacticCoord.ofSectorLocal(starId, 0L, 0L, 0L, 0L, 0L); - int orbit = PlanetDerivation.orbitalDistanceOf(server.getWorld(0).getSeed(), anchor, index, + zmaster587.advancedRocketry.universe.IBodyDerivation derivation = + zmaster587.advancedRocketry.universe.UniverseRegistry.getGenerator().derivation(); + int orbit = derivation.orbitalDistanceOf(server.getWorld(0).getSeed(), anchor, index, Math.max(1, index + 1), star); - BodyProfile profile = PlanetDerivation.derive(server.getWorld(0).getSeed(), anchor, anchor, + BodyProfile profile = derivation.derive(server.getWorld(0).getSeed(), anchor, anchor, index, star, moon, orbit); int dimId = DimensionManager.getInstance().getNextFreeDim(2); diff --git a/src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseCommand.java b/src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseCommand.java new file mode 100644 index 000000000..f37f57056 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseCommand.java @@ -0,0 +1,28 @@ +package zmaster587.advancedRocketry.command.sub.universe; + +import net.minecraft.command.ICommandSender; +import net.minecraftforge.server.command.CommandTreeBase; +import net.minecraftforge.server.command.CommandTreeHelp; + +/** + * Operator commands for the world model a save was generated under: what it is, and how to move a + * world onto a newer one deliberately. + */ +public class UniverseCommand extends CommandTreeBase { + + public UniverseCommand() { + addSubcommand(new UniverseStatusCommand()); + addSubcommand(new UniverseUpgradeCommand()); + addSubcommand(new CommandTreeHelp(this)); + } + + @Override + public String getName() { + return "universe"; + } + + @Override + public String getUsage(ICommandSender sender) { + return "commands.advancedrocketry.universe.usage"; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseStatusCommand.java b/src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseStatusCommand.java new file mode 100644 index 000000000..e224420d9 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseStatusCommand.java @@ -0,0 +1,76 @@ +package zmaster587.advancedRocketry.command.sub.universe; + +import java.util.List; + +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.text.TextComponentTranslation; +import zmaster587.advancedRocketry.command.sub.ARCommand; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.UniverseRegistry; +import zmaster587.advancedRocketry.universe.UniverseSchemas; + +/** + * What world model this save runs on, what the pack currently states, and how much of the universe has + * already been frozen by being seen. + * + *

    Read-only, and the first thing to run when a load has been refused: it names both sides of the + * comparison that refused it.

    + */ +public class UniverseStatusCommand extends ARCommand { + + @Override + public String getName() { + return "status"; + } + + @Override + public String getUsage(ICommandSender sender) { + return "commands.advancedrocketry.universe.status.usage"; + } + + @Override + public void execute(MinecraftServer server, ICommandSender sender, String[] args) + throws CommandException { + if (args.length > 0) { + throw wrongUsage(sender); + } + UniverseRegistry registry = UniverseRegistry.get(server); + if (registry == null) { + throw new CommandException("commands.advancedrocketry.universe.unavailable"); + } + GalaxyGenConfig pack = UniverseRegistry.packGalaxyConfig(); + String packFingerprint = UniverseRegistry.fingerprintOf(pack); + + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.status.schema", + registry.schemaVersion(), UniverseSchemas.CURRENT)); + UniverseRegistry.activeSchema().ifPresent(schema -> sender.sendMessage( + new TextComponentTranslation(schema.isStable() + ? "commands.advancedrocketry.universe.status.stable" + : "commands.advancedrocketry.universe.status.alpha", schema.label()))); + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.status.config", + registry.configFingerprint(), packFingerprint)); + sender.sendMessage(new TextComponentTranslation( + registry.configFingerprint().equals(packFingerprint) + ? "commands.advancedrocketry.universe.status.agrees" + : "commands.advancedrocketry.universe.status.differs")); + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.status.frozen", registry.pinnedSystemCount())); + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.status.released", + UniverseSchemas.released().toString())); + if (registry.isUpgradeArmed()) { + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.status.armed")); + } + } + + @Override + public List getTabCompletions(MinecraftServer server, ICommandSender sender, String[] args, + net.minecraft.util.math.BlockPos targetPos) { + return java.util.Collections.emptyList(); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseUpgradeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseUpgradeCommand.java new file mode 100644 index 000000000..baee72bc1 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/command/sub/universe/UniverseUpgradeCommand.java @@ -0,0 +1,132 @@ +package zmaster587.advancedRocketry.command.sub.universe; + +import java.util.List; + +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.text.TextComponentTranslation; +import zmaster587.advancedRocketry.command.sub.ARCommand; +import zmaster587.advancedRocketry.item.ItemMemoryCrystal; +import zmaster587.advancedRocketry.navigation.CrystalEntry; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.UniverseRegistry; +import zmaster587.advancedRocketry.universe.UniverseSchema; + +/** + * Move this world onto the model the pack and this build now state — deliberately, and only after + * everything already seen has been frozen where it stands. + * + *

    What it does, in order. Every address anybody has written down is pinned first: the systems + * already in the override store are immutable by construction, and every address on a memory crystal is + * pinned here. Only then is the new stamp written. The result is a seam at the frontier of the + * explored — charted space keeps its contents, unexplored space is re-derived under the new model — + * and that seam is the player's own choice, which is why this is a command and not a migration that + * runs itself at load. + * + *

    What it cannot reach, and says so. A crystal in a chest, in an unloaded chunk, or in the + * inventory of a player who is offline is not readable from here. Bring the crystals that matter to + * players who are online before running it. + * + *

    What arrives without content. Mechanics a newer model introduces do not retrofit into space + * that is already frozen: a world upgraded halfway through a campaign keeps its charted systems exactly + * as they were, and meets the new ones only further out. That belongs in a changelog, not in a fix. + */ +public class UniverseUpgradeCommand extends ARCommand { + + private static final String CONFIRM = "confirm"; + + @Override + public String getName() { + return "upgrade"; + } + + @Override + public String getUsage(ICommandSender sender) { + return "commands.advancedrocketry.universe.upgrade.usage"; + } + + @Override + public void execute(MinecraftServer server, ICommandSender sender, String[] args) + throws CommandException { + if (args.length > 1 || (args.length == 1 && !CONFIRM.equalsIgnoreCase(args[0]))) { + throw wrongUsage(sender); + } + UniverseRegistry registry = UniverseRegistry.get(server); + if (registry == null) { + throw new CommandException("commands.advancedrocketry.universe.unavailable"); + } + GalaxyGenConfig pack = UniverseRegistry.packGalaxyConfig(); + String target = UniverseRegistry.fingerprintOf(pack); + + if (args.length == 0) { + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.upgrade.preview", + registry.configFingerprint(), target, registry.pinnedSystemCount())); + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.upgrade.reach", + server.getPlayerList().getCurrentPlayerCount())); + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.upgrade.confirm")); + return; + } + + int crystals = 0; + int addresses = 0; + int frozen = 0; + for (EntityPlayerMP player : server.getPlayerList().getPlayers()) { + for (ItemStack stack : carried(player)) { + if (!ItemMemoryCrystal.isCrystal(stack)) { + continue; + } + crystals++; + for (CrystalEntry entry : ItemMemoryCrystal.memoryOf(stack).list()) { + addresses++; + if (registry.pinSystem(entry.coord())) { + frozen++; + } + } + } + } + + int wasVersion = registry.schemaVersion(); + UniverseSchema schema = registry.adoptSchema(pack); + // A schema version can be moved here and now: this build carries the new one, so the world can + // start deriving under it immediately rather than after a restart. + UniverseRegistry.setGenerator(schema.generator(pack)); + // A CONFIGURATION change cannot be seen from inside a server that is running — a changed + // stops the load before this command can be typed. So the permission is left here + // for that load to spend. + registry.armUpgrade(); + + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.upgrade.done", + crystals, addresses, frozen, wasVersion, schema.version(), target)); + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.upgrade.armed")); + sender.sendMessage(new TextComponentTranslation( + "commands.advancedrocketry.universe.upgrade.seam")); + } + + /** Every stack a player has on him — held, worn, and in his ender chest. */ + private static Iterable carried(EntityPlayerMP player) { + List all = new java.util.ArrayList<>(); + all.addAll(player.inventory.mainInventory); + all.addAll(player.inventory.offHandInventory); + all.addAll(player.inventory.armorInventory); + for (int i = 0; i < player.getInventoryEnderChest().getSizeInventory(); i++) { + all.add(player.getInventoryEnderChest().getStackInSlot(i)); + } + return all; + } + + @Override + public List getTabCompletions(MinecraftServer server, ICommandSender sender, String[] args, + net.minecraft.util.math.BlockPos targetPos) { + return args.length == 1 + ? getListOfStringsMatchingLastWord(args, CONFIRM) + : java.util.Collections.emptyList(); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/command/sub/universe/package-info.java b/src/main/java/zmaster587/advancedRocketry/command/sub/universe/package-info.java new file mode 100644 index 000000000..610e6c1d8 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/command/sub/universe/package-info.java @@ -0,0 +1,5 @@ +/** + * Operator commands for the world model a save was generated under — reporting it, and moving a world + * onto a newer one deliberately. + */ +package zmaster587.advancedRocketry.command.sub.universe; diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 8d36ba618..27044ecd7 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -5192,7 +5192,8 @@ private void handleSpace(MinecraftServer server, ICommandSender sender, String[] return; } zmaster587.advancedRocketry.universe.BodyProfile p = - zmaster587.advancedRocketry.universe.PlanetDerivation.derive(reg.worldSeed(), + zmaster587.advancedRocketry.universe.UniverseRegistry.getGenerator() + .derivation().derive(reg.worldSeed(), anchor.get(), target.name(), variant, star.get(), target.kind() == zmaster587.advancedRocketry.universe.SystemBodyKind.MOON, target.orbitalDistance()); diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java index 745da5e7b..890cabf74 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionManager.java @@ -923,14 +923,20 @@ public void createAndLoadDimensions(boolean resetFromXml) { zmaster587.advancedRocketry.universe.UniverseRegistry.stageAnchors(dimCouplingList.anchorCoords, resetFromXml); } - // Install the procedural galaxy generator when the pack opts in via ; otherwise reset to - // the authored-anchors-only default. The generator is a JVM-global, so reset every load so a world - // without never inherits a previous world's generator. + // Hand the pack's knobs to the universe layer. The generator built from them is + // installed for real at populate(), because WHICH world model interprets these knobs is a + // property of the SAVE (its schema stamp) and the save is not reachable here — worlds are not + // loaded yet. The pack states the parameters; the world states the version. + // + // The provisional install below keeps this window behaving exactly as it did before the stamp + // existed: the generator is a JVM-global, so it is reset every load and a world without + // never inherits a previous world's generator. populate() then replaces it with the + // generator the save is actually owed, before anything derives. zmaster587.advancedRocketry.universe.GalaxyGenConfig galaxyGenConfig = (dimCouplingList != null) ? dimCouplingList.galaxyGenConfig : null; - zmaster587.advancedRocketry.universe.UniverseRegistry.setGenerator(galaxyGenConfig == null - ? null - : new zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator(galaxyGenConfig)); + zmaster587.advancedRocketry.universe.UniverseRegistry.stageGalaxyConfig(galaxyGenConfig); + zmaster587.advancedRocketry.universe.UniverseRegistry.setGenerator( + zmaster587.advancedRocketry.universe.UniverseSchemas.current().generator(galaxyGenConfig)); // C129: registration authority on load was planetDefs.xml only (the loop // above), while per-dim persisted state lives in temp.dat (loadedPlanets). // A dim present in temp.dat but absent from a hand-edited / restored / diff --git a/src/main/java/zmaster587/advancedRocketry/space/SkyNebulaeProducer.java b/src/main/java/zmaster587/advancedRocketry/space/SkyNebulaeProducer.java index 8635b6c87..39eb0b5a0 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/SkyNebulaeProducer.java +++ b/src/main/java/zmaster587/advancedRocketry/space/SkyNebulaeProducer.java @@ -91,9 +91,12 @@ public static List around(IGalaxyGenerator generator, long seed, return Collections.emptyList(); } GalacticCoord c = cell.cellCentre(); - double observerX = UniverseScale.lightYearsForCells(c.sectorX()); - double observerY = UniverseScale.lightYearsForCells(c.sectorY()); - double observerZ = UniverseScale.lightYearsForCells(c.sectorZ()); + // Measured by the generator that produced these clouds, not by a global: a sky drawn under + // one schema's metric and clouds seated under another's would not line up. + zmaster587.advancedRocketry.universe.IUniverseLaws laws = generator.laws(); + double observerX = laws.lightYearsForCells(c.sectorX()); + double observerY = laws.lightYearsForCells(c.sectorY()); + double observerZ = laws.lightYearsForCells(c.sectorZ()); List out = new ArrayList<>(); for (Nebula nebula : found) { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/BodyDerivationV0.java b/src/main/java/zmaster587/advancedRocketry/universe/BodyDerivationV0.java new file mode 100644 index 000000000..d4613e027 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/BodyDerivationV0.java @@ -0,0 +1,79 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.space.GalacticCoord; + +/** + * Schema version 0's body derivation — every law exactly as {@link PlanetDerivation} states it. + * + *

    A pure forwarder, and deliberately so: the arithmetic stays in one place, where its constants are + * documented next to the observations they come from, and this class is only the handle a schema holds + * it by. A version 2 is a second implementation of {@link IBodyDerivation}, not an edit here. + * + *

    Stateless, so one instance serves every world. + */ +public final class BodyDerivationV0 implements IBodyDerivation { + + public static final BodyDerivationV0 INSTANCE = new BodyDerivationV0(); + + private BodyDerivationV0() { + } + + @Override + public double metallicityOf(long seed, GalacticCoord anchor) { + return PlanetDerivation.metallicityOf(seed, anchor); + } + + @Override + public int referenceDistance(StellarBody star) { + return PlanetDerivation.referenceDistance(star); + } + + @Override + public int orbitalDistanceOf(long seed, GalacticCoord anchor, int index, int count, + StellarBody star) { + return PlanetDerivation.orbitalDistanceOf(seed, anchor, index, count, star); + } + + @Override + public double innerOrbit(StellarBody star) { + return PlanetDerivation.innerOrbit(star); + } + + @Override + public double outerOrbit(StellarBody star) { + return PlanetDerivation.outerOrbit(star); + } + + @Override + public int bareTemperature(StellarBody star, int orbitalDistance) { + return PlanetDerivation.bareTemperature(star, orbitalDistance); + } + + @Override + public boolean tidallyLockedAt(StellarBody star, int orbitalDistance) { + return PlanetDerivation.tidallyLockedAt(star, orbitalDistance); + } + + @Override + public boolean isGiantAt(long seed, GalacticCoord anchor, int index, int bareTemperatureK) { + return PlanetDerivation.isGiantAt(seed, anchor, index, bareTemperatureK); + } + + @Override + public BodyProfile derive(long seed, GalacticCoord anchor, GalacticCoord bodyCell, int variant, + StellarBody star, boolean moon, int orbitalDistance) { + return PlanetDerivation.derive(seed, anchor, bodyCell, variant, star, moon, orbitalDistance); + } + + @Override + public BodyProfile deriveRogue(long seed, GalacticCoord bodyCell, int variant, + double giantFraction) { + return PlanetDerivation.deriveRogue(seed, bodyCell, variant, giantFraction); + } + + @Override + public int residualTemperature(double massEarths, double radiusEarths) { + return PlanetDerivation.residualTemperature(massEarths, radiusEarths); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/CellHash.java b/src/main/java/zmaster587/advancedRocketry/universe/CellHash.java index b91e8e61e..946c238e8 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/CellHash.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/CellHash.java @@ -14,14 +14,19 @@ * constant here silently moves stars and reshapes planets in every existing save that has not been * touched. It lives in one place for exactly that reason: two copies of a mixer are two things to * forget about.

    + * + *

    Public because it belongs to every SCHEMA, not to one generator. A released world model is + * kept reproducible forever, and a later version that changes what it draws still has to draw it out of + * the same mixer — a second copy of this arithmetic in another package would be a second thing to keep + * in step, which is exactly what the paragraph above forbids.

    */ -final class CellHash { +public final class CellHash { private CellHash() { } /** Mix {@code seed}, the triple {@code (a,b,c)} and {@code salt} into a uniform 64-bit value. */ - static long of(long seed, long a, long b, long c, long salt) { + public static long of(long seed, long a, long b, long c, long salt) { long h = seed + salt * 0x9E3779B97F4A7C15L; h ^= a; h *= 0xFF51AFD7ED558CCDL; @@ -36,7 +41,7 @@ static long of(long seed, long a, long b, long c, long salt) { } /** Mix a cell's own field draw. */ - static long ofCell(long seed, GalacticCoord cell, long salt) { + public static long ofCell(long seed, GalacticCoord cell, long salt) { return of(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ(), salt); } @@ -47,13 +52,13 @@ static long ofCell(long seed, GalacticCoord cell, long salt) { * the salt, so the two cannot merge into {@code (i + salt) * G} and correlate neighbouring bodies' * draws — which would make body {@code i}'s radius a near-copy of body {@code i+1}'s.

    */ - static long ofBody(long seed, GalacticCoord cell, int index, long salt) { + public static long ofBody(long seed, GalacticCoord cell, int index, long salt) { return of(seed ^ (index * 0xD1B54A32D192ED03L), cell.sectorX(), cell.sectorY(), cell.sectorZ(), salt); } /** Map a 64-bit hash to a double in {@code [0, 1)}. */ - static double norm(long h) { + public static double norm(long h) { return (h >>> 11) * 0x1.0p-53; } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java index 65ce6ec4e..1348ea17c 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java @@ -29,6 +29,8 @@ public final class ClusterField { private static final long SALT_NUCLEUS_RADIUS = 0x207L; private final GalaxyGenConfig config; + /** The metric this field measures with — its schema's, not a global one. */ + private final IUniverseLaws laws; private final GalaxyField galaxies; private final long spacingSuperCells; @@ -37,9 +39,10 @@ public final class ClusterField { * galaxy is scaled by that galaxy's profile; one out in the void is scaled by the * ejecta halo, which is how a globular can be intergalactic without a second rule */ - public ClusterField(GalaxyGenConfig config, GalaxyField galaxies) { + public ClusterField(GalaxyGenConfig config, GalaxyField galaxies, IUniverseLaws laws) { + this.laws = (laws == null) ? UniverseLawsV0.INSTANCE : laws; this.config = (config == null) ? GalaxyGenConfig.defaults() : config; - this.galaxies = (galaxies == null) ? new GalaxyField(this.config) : galaxies; + this.galaxies = (galaxies == null) ? new GalaxyField(this.config, this.laws) : galaxies; this.spacingSuperCells = Math.max(1L, superCellsForLightYears(GalaxyGenConfig.CLUSTER_SPACING_LY, this.config.minSpacing)); } @@ -177,8 +180,8 @@ public long spacingSuperCells() { } /** A length in light years as a whole number of coarse super-cells, at least one. */ - private static long superCellsForLightYears(double lightYears, long superCellEdgeCells) { - long cells = UniverseScale.cellsForLightYears(lightYears); + private long superCellsForLightYears(double lightYears, long superCellEdgeCells) { + long cells = laws.cellsForLightYears(lightYears); return Math.max(1L, cells / Math.max(1L, superCellEdgeCells)); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index edaeca2b4..f6ad0cdd1 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -188,6 +188,8 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { private static final int MAX_SYSTEMS_PER_REGION_QUERY = 20_000; private final GalaxyGenConfig config; + private final IBodyDerivation derivation; + private final IUniverseLaws laws; private final GalaxyField galaxies; private final ClusterField clusters; private final NebulaField nebulae; @@ -195,11 +197,31 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { private final List rogueTypes; private final long totalRogueWeight; + /** + * The stock generator: version 1's body derivation. Kept so every existing call site and test + * reads unchanged; a schema that means something else says so with the constructor below. + */ public ClusteredGalaxyGenerator(GalaxyGenConfig config) { + this(config, BodyDerivationV0.INSTANCE, UniverseLawsV0.INSTANCE); + } + + /** The same field with a stated derivation, measuring by version 1's laws. */ + public ClusteredGalaxyGenerator(GalaxyGenConfig config, IBodyDerivation derivation) { + this(config, derivation, UniverseLawsV0.INSTANCE); + } + + /** + * The full form: a field that derives its bodies by {@code derivation} and measures by + * {@code laws} — the two halves a later schema version differs in. + */ + public ClusteredGalaxyGenerator(GalaxyGenConfig config, IBodyDerivation derivation, + IUniverseLaws laws) { + this.derivation = (derivation == null) ? BodyDerivationV0.INSTANCE : derivation; + this.laws = (laws == null) ? UniverseLawsV0.INSTANCE : laws; this.config = (config == null) ? GalaxyGenConfig.defaults() : config; - this.galaxies = new GalaxyField(this.config); - this.clusters = new ClusterField(this.config, this.galaxies); - this.nebulae = new NebulaField(this.config, this.clusters); + this.galaxies = new GalaxyField(this.config, this.laws); + this.clusters = new ClusterField(this.config, this.galaxies, this.laws); + this.nebulae = new NebulaField(this.config, this.clusters, this.laws); long w = 0L; // accumulate in long so a few near-Integer.MAX weights cannot overflow the sum for (GalaxyGenConfig.StarType t : this.config.starTypes) { w += t.weight; @@ -213,6 +235,21 @@ public ClusteredGalaxyGenerator(GalaxyGenConfig config) { this.totalRogueWeight = Math.max(1L, rw); } + @Override + public IBodyDerivation derivation() { + return derivation; + } + + @Override + public IUniverseLaws laws() { + return laws; + } + + @Override + public java.util.Optional tuning() { + return java.util.Optional.of(config); + } + public GalaxyGenConfig config() { return config; } @@ -399,10 +436,10 @@ public List bodiesFor(long seed, GalacticCoord systemCoord) { * inner few and nothing else — the same ceiling a rocky world has, applied whatever its bulk, * rather than the ceiling its mass would otherwise buy it.

    */ - private static List rogueBodiesFor(long seed, GalacticCoord cell, int systemId, + private List rogueBodiesFor(long seed, GalacticCoord cell, int systemId, double giantFraction) { List bodies = new ArrayList<>(); - BodyProfile profile = PlanetDerivation.deriveRogue(seed, cell, 0, giantFraction); + BodyProfile profile = derivation.deriveRogue(seed, cell, 0, giantFraction); // It does not move inside its own system: it IS the system, so its frame is the anchor's. bodies.add(SystemBody.fixedAt(cell, SystemBodyKind.ROGUE_PLANET, Constants.INVALID_PLANET, systemId).withRadius(profile.radiusEarths())); @@ -425,7 +462,7 @@ private static List rogueBodiesFor(long seed, GalacticCoord cell, in SystemContent.MOON_UNIT_BLOCKS); // A moon of a rogue is starless too, so it is derived the same way its parent was, one // variant along — never through the star-lit law with a star that is not there. - BodyProfile moonProfile = PlanetDerivation.deriveRogue(seed, cell, j, giantFraction); + BodyProfile moonProfile = derivation.deriveRogue(seed, cell, j, giantFraction); bodies.add(new SystemBody(cell, frame, law, SystemBodyKind.MOON, Constants.INVALID_PLANET, systemId, SystemBody.ORBIT_UNKNOWN) .withRadius(moonProfile.radiusEarths())); @@ -461,7 +498,7 @@ private void appendRetinue(List bodies, long seed, GalacticCoord cel // moved it inward, which is the one thing this whole seam exists to prevent: a world's // distance is its star's business, and a system squeezed by its neighbours holds fewer // worlds rather than the same worlds at the wrong distances. - int orbit = PlanetDerivation.orbitalDistanceOf(seed, cell, i, count, star); + int orbit = derivation.orbitalDistanceOf(seed, cell, i, count, star); if (orbit > outerBound) { continue; // outside this system's clear space — a bound of the layout, not a failure } @@ -476,7 +513,7 @@ private void appendRetinue(List bodies, long seed, GalacticCoord cel // which is what makes the zoning (rock inside, giants past the snow line) emerge instead // of being authored. Kept here rather than at realization because the nav list, the sky // and the descent trigger all read the kind long before anyone lands. - BodyProfile profile = PlanetDerivation.derive(seed, cell, seat.cell, 0, star, false, orbit); + BodyProfile profile = derivation.derive(seed, cell, seat.cell, 0, star, false, orbit); // THE ORBIT LIVES IN THE FRAME, not in the body's own offset — the same shape an authored // system uses (SystemContent: a planet sits at its frame origin and the FRAME goes round // the star). Built with the convenience constructor, a procedural planet got @@ -512,7 +549,7 @@ private void appendRetinue(List bodies, long seed, GalacticCoord cel // beyond the outermost world; what it may NOT pass is the system's own clear space, and there // it is bounded like everything else rather than being quietly dropped. double outerBelt = Math.max(outermostOrbit * OUTER_BELT_FACTOR, - PlanetDerivation.innerOrbit(star) * 2d); + derivation.innerOrbit(star) * 2d); addBelt(bodies, seed, cell, (int) Math.min(outerBelt, outerBound), star, lattice, starId, taken, count + 2); } @@ -561,10 +598,10 @@ public static int retinueSize(long seed, GalacticCoord anchor) { * How far this system's NAMED bodies may reach from its star, in orbital-distance units: the * declared clear space around a seat, or as much of it as this spacing can actually give. */ - private static double maxNamedOrbitUnits(long s) { - long reachCells = Math.max(1L, UniverseScale.seatMarginCells(s) - NEIGHBOURHOOD_MARGIN_CELLS); + private double maxNamedOrbitUnits(long s) { + long reachCells = Math.max(1L, laws.seatMarginCells(s) - NEIGHBOURHOOD_MARGIN_CELLS); return Math.min(UniverseScale.MAX_NAMED_ORBIT_UNITS, - UniverseScale.orbitUnitsForCells(reachCells)); + laws.orbitUnitsForCells(reachCells)); } /** @@ -679,7 +716,7 @@ private void addMoons(List bodies, long seed, GalacticCoord anchor, // It used to ride a static frame of its own, which pinned the whole family in place. // A moon's size comes from the SAME derivation a descent will realize it with, so the // moon a pilot sees from orbit is the moon he lands on. - BodyProfile moonProfile = PlanetDerivation.derive(seed, anchor, parent, j, star, true, + BodyProfile moonProfile = derivation.derive(seed, anchor, parent, j, star, true, parentOrbit); bodies.add(new SystemBody(parent, parentFrame, law, SystemBodyKind.MOON, Constants.INVALID_PLANET, starId, parentOrbit) @@ -700,9 +737,9 @@ public BodyProfile profileOf(long seed, GalacticCoord anchor, SystemBody body, S // Nothing lights this system, so nothing about the body follows from a distance: it is the // starless derivation or it is a body whose physics would be read off a star that is not // there. A moon of a rogue takes the same branch, which is right — it is starless too. - return PlanetDerivation.deriveRogue(seed, body.name(), variant, config.rogue.giantFraction); + return derivation.deriveRogue(seed, body.name(), variant, config.rogue.giantFraction); } - return PlanetDerivation.derive(seed, anchor.cellCentre(), body.name(), variant, star, + return derivation.derive(seed, anchor.cellCentre(), body.name(), variant, star, body.kind() == SystemBodyKind.MOON, body.orbitalDistance()); } @@ -726,7 +763,7 @@ public List nebulaeAround(long seed, GalacticCoord cell, double radiusLy return Collections.emptyList(); } long s = config.minSpacing; - long reachSuper = Math.max(1L, UniverseScale.cellsForLightYears(radiusLy) / s); + long reachSuper = Math.max(1L, laws.cellsForLightYears(radiusLy) / s); long supX = Math.floorDiv(c.sectorX(), s); long supY = Math.floorDiv(c.sectorY(), s); long supZ = Math.floorDiv(c.sectorZ(), s); @@ -897,7 +934,7 @@ private Optional rogueForLattice(long seed, Lattice lattice, double p * in a globular core really do stand closer than a wide binary, and a system there loses outer * bodies by the same rule that has always applied.

    */ - private static GalacticCoord seatIn(long seed, Lattice lattice) { + private GalacticCoord seatIn(long seed, Lattice lattice) { return GalacticCoord.ofSectorLocal( lattice.lowX + seatOffset(seed, lattice, SALT_OX, lattice.edgeX), lattice.lowY + seatOffset(seed, lattice, SALT_OY, lattice.edgeY), @@ -929,8 +966,8 @@ private GalaxyGenConfig.RogueType pickRogueType(long h) { } /** Where the seat sits on one axis of its lattice cell, clear of the faces by the local margin. */ - private static long seatOffset(long seed, Lattice lattice, long salt, long edge) { - long margin = UniverseScale.seatMarginCells(edge); + private long seatOffset(long seed, Lattice lattice, long salt, long edge) { + long margin = laws.seatMarginCells(edge); long band = Math.max(1L, edge - 2L * margin); return margin + Math.floorMod(lattice.hash(seed, salt), band); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Fingerprint.java b/src/main/java/zmaster587/advancedRocketry/universe/Fingerprint.java new file mode 100644 index 000000000..612c4be93 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/Fingerprint.java @@ -0,0 +1,46 @@ +package zmaster587.advancedRocketry.universe; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; + +/** + * How this layer turns a set of numbers into a short identity a save can carry. + * + *

    One place, because two digests of the same kind computed two ways are two things to keep in step, + * and a stamp that disagrees with itself between builds is worse than no stamp. + * + *

    Stable across JVMs and versions by construction. No {@link Object#hashCode()} anywhere + * (identity hashes and even {@code String.hashCode} are not promised across implementations), doubles + * rendered through {@link Double#doubleToLongBits} rather than formatted (no locale, no rounding, and + * the last bit is visible), and every caller renders its lists in a declared order — order is part of + * an identity whenever a weighted table is walked by it. + */ +final class Fingerprint { + + private Fingerprint() { + } + + /** A double as its exact bits — the only rendering that neither rounds nor asks about a locale. */ + static String bits(double v) { + return Long.toHexString(Double.doubleToLongBits(v)); + } + + /** 16 lowercase hex of SHA-256 — short enough to read out of a log, long enough not to collide. */ + static String hex16(String canonical) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] out = md.digest(canonical.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(16); + for (int i = 0; i < 8; i++) { + hex.append(String.format(Locale.ROOT, "%02x", out[i])); + } + return hex.toString(); + } catch (NoSuchAlgorithmException e) { + // SHA-256 is mandated by the Java platform. If it is genuinely absent the stamp cannot be + // computed, and a silent fallback would be a value that compares equal against everything. + throw new IllegalStateException("SHA-256 unavailable, cannot fingerprint the universe", e); + } + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalacticAnchor.java b/src/main/java/zmaster587/advancedRocketry/universe/GalacticAnchor.java index f2c5c6d73..03ae99949 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalacticAnchor.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalacticAnchor.java @@ -65,9 +65,10 @@ public GalacticCoord resolve(Optional centre) { * minimum radius is checked against. */ public double reachLy() { - double x = UniverseScale.lightYearsForCells(local.sectorX()); - double y = UniverseScale.lightYearsForCells(local.sectorY()); - double z = UniverseScale.lightYearsForCells(local.sectorZ()); + IUniverseLaws laws = UniverseRegistry.getGenerator().laws(); + double x = laws.lightYearsForCells(local.sectorX()); + double y = laws.lightYearsForCells(local.sectorY()); + double z = laws.lightYearsForCells(local.sectorZ()); return Math.sqrt(x * x + y * y + z * z); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java index 94564a46d..c7270e35f 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java @@ -65,6 +65,8 @@ public final class Galaxy { */ public static final double EDGE_LEVEL = Math.exp(-1d / DISC_SCALE_FRACTION) / REFERENCE_LEVEL; + /** The metric this object was seated under — its schema's, never a global one. */ + private final IUniverseLaws laws; private final long cellX; private final long cellY; private final long cellZ; @@ -108,13 +110,15 @@ public final class Galaxy { */ public Galaxy(long cellX, long cellY, long cellZ, int satelliteIndex, GalacticCoord centre, GalaxyGenConfig.GalaxyType type, double radiusLy, double tilt, double node, - double armPitch, double armPhase, LightYearVector peculiarVelocity) { + double armPitch, double armPhase, LightYearVector peculiarVelocity, + IUniverseLaws laws) { + this.laws = (laws == null) ? UniverseLawsV0.INSTANCE : laws; this.cellX = cellX; this.cellY = cellY; this.cellZ = cellZ; this.satelliteIndex = Math.max(0, satelliteIndex); this.centre = centre; - this.seat = LightYearVector.ofCell(centre); + this.seat = LightYearVector.ofCell(centre, this.laws); this.peculiarVelocity = (peculiarVelocity == null) ? LightYearVector.ZERO : peculiarVelocity; this.type = type; this.radiusLy = Math.max(1d, radiusLy); @@ -362,7 +366,7 @@ private double armFactor(double r, double theta) { */ public double angularSpeedAt(double rLy) { double core = radiusLy * type.coreRadiusFraction; - double speed = UniverseScale.lightYearsPerTick(type.rotationSpeedKmS); + double speed = laws.lightYearsPerTick(type.rotationSpeedKmS); return speed / Math.hypot(Math.max(0d, rLy), core); } @@ -396,7 +400,7 @@ public double rotationPeriodTicks(double rLy) { */ public LightYearVector centreAt(long tick) { return seat.plus(peculiarVelocity.scale((double) tick)) - .scale(Cosmology.scaleFactorAt(tick)); + .scale(laws.scaleFactorAt(tick)); } /** @@ -449,8 +453,8 @@ public LightYearVector boundPositionOfCellAt(GalacticCoord cell, long tick) { // ─── Helpers ─────────────────────────────────────────────────────────────── /** A sector delta as a length in light years. Exact: the delta is bounded by one galaxy cell. */ - private static double offsetLy(long sector, long centreSector) { - return UniverseScale.lightYearsForCells((double) (sector - centreSector)); + private double offsetLy(long sector, long centreSector) { + return laws.lightYearsForCells((double) (sector - centreSector)); } private static double atLeastZero(double v) { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java index 275cf0bcc..5503f9a0f 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyField.java @@ -101,10 +101,13 @@ public final class GalaxyField { private static final double MAX_PECULIAR_SPEED_KM_S = 600d; private final GalaxyGenConfig config; + /** The metric this field measures with — its schema's, not a global one. */ + private final IUniverseLaws laws; private final long totalGalaxyWeight; private final long totalHomeWeight; - public GalaxyField(GalaxyGenConfig config) { + public GalaxyField(GalaxyGenConfig config, IUniverseLaws laws) { + this.laws = (laws == null) ? UniverseLawsV0.INSTANCE : laws; this.config = (config == null) ? GalaxyGenConfig.defaults() : config; long all = 0L; // accumulated in long so a few near-Integer.MAX weights cannot overflow the sum long home = 0L; @@ -381,9 +384,9 @@ private Galaxy satelliteOf(long seed, Galaxy primary, int ordinal) { double offZ = distanceLy * sinEl * Math.sin(heading); GalacticCoord centre = GalacticCoord.ofSectorLocal( - primary.centre().sectorX() + UniverseScale.cellsAt(offX), - primary.centre().sectorY() + UniverseScale.cellsAt(offY), - primary.centre().sectorZ() + UniverseScale.cellsAt(offZ), 0L, 0L, 0L); + primary.centre().sectorX() + laws.cellsAt(offX), + primary.centre().sectorY() + laws.cellsAt(offY), + primary.centre().sectorZ() + laws.cellsAt(offZ), 0L, 0L, 0L); double tilt = Math.acos(2d * CellHash.norm( CellHash.of(ownSeed, gx, gy, gz, SALT_SATELLITE_TILT)) - 1d); @@ -396,7 +399,7 @@ private Galaxy satelliteOf(long seed, Galaxy primary, int ordinal) { * 2d * Math.PI; return new Galaxy(gx, gy, gz, ordinal, centre, type, radiusLy, tilt, node, pitch, phase, - primary.peculiarVelocity()); + primary.peculiarVelocity(), laws); } /** @@ -434,14 +437,14 @@ private GalaxyGenConfig.GalaxyType pickSatelliteType(long seed, long gx, long gy * Whether a point is close enough to {@code primary} for any of its satellites to reach it. One * sphere test that rejects the whole retinue, so the void inside a cube costs nothing. */ - private static boolean withinRetinueReach(Galaxy primary, long sectorX, long sectorY, + private boolean withinRetinueReach(Galaxy primary, long sectorX, long sectorY, long sectorZ) { - double reach = UniverseScale.retinueReachLy(primary.radiusLy()); - double dx = UniverseScale.lightYearsForCells( + double reach = laws.retinueReachLy(primary.radiusLy()); + double dx = laws.lightYearsForCells( (double) (sectorX - primary.centre().sectorX())); - double dy = UniverseScale.lightYearsForCells( + double dy = laws.lightYearsForCells( (double) (sectorY - primary.centre().sectorY())); - double dz = UniverseScale.lightYearsForCells( + double dz = laws.lightYearsForCells( (double) (sectorZ - primary.centre().sectorZ())); return dx * dx + dy * dy + dz * dz <= reach * reach; } @@ -526,7 +529,7 @@ public Optional galaxyAtIndex(long seed, long gx, long gy, long gz) { return Optional.of(new Galaxy(gx, gy, gz, 0, seatOf(seed, gx, gy, gz, radiusLy, tilt, node, home), type, radiusLy, tilt, node, pitch, phase, - peculiarVelocityOf(seed, gx, gy, gz, radiusLy, home))); + peculiarVelocityOf(seed, gx, gy, gz, radiusLy, home), laws)); } /** @@ -549,8 +552,8 @@ private LightYearVector peculiarVelocityOf(long seed, long gx, long gy, long gz, double u = CellHash.norm(CellHash.of(seed, gx, gy, gz, SALT_GALAXY_SPEED)); double kmPerSecond = MIN_PECULIAR_SPEED_KM_S + u * (MAX_PECULIAR_SPEED_KM_S - MIN_PECULIAR_SPEED_KM_S); - double speed = Math.min(UniverseScale.lightYearsPerTick(kmPerSecond), - driftBudgetLy(radiusLy) / (double) Cosmology.DRIFT_HORIZON_TICKS); + double speed = Math.min(laws.lightYearsPerTick(kmPerSecond), + driftBudgetLy(radiusLy) / (double) laws.driftHorizonTicks()); // Isotropic: cos(elevation) uniform, not the elevation itself, or the draws would pile up at // the poles of whatever axis happened to be written first. @@ -566,8 +569,8 @@ private LightYearVector peculiarVelocityOf(long seed, long gx, long gy, long gz, * whole group travels together, so the budget is the group's reach and not the primary's radius. */ private double driftBudgetLy(double radiusLy) { - double halfCellLy = UniverseScale.lightYearsForCells(config.galaxySpacing / 2d); - return Math.max(0d, halfCellLy - UniverseScale.retinueReachLy(radiusLy)); + double halfCellLy = laws.lightYearsForCells(config.galaxySpacing / 2d); + return Math.max(0d, halfCellLy - laws.retinueReachLy(radiusLy)); } // ─── The intergalactic regime ────────────────────────────────────────────── @@ -606,8 +609,8 @@ public LightYearVector positionAt(long seed, GalacticCoord cell, long tick) { * is orders past what a block {@code long} holds, and the layer never asks one to hold it. The * cell NAME carries the magnitude (a sector triple) and this vector carries the rest.

    */ - public static LightYearVector comovingPositionAt(GalacticCoord cell, long tick) { - return LightYearVector.ofCell(cell).scale(Cosmology.scaleFactorAt(tick)); + public LightYearVector comovingPositionAt(GalacticCoord cell, long tick) { + return LightYearVector.ofCell(cell, laws).scale(laws.scaleFactorAt(tick)); } /** @@ -659,12 +662,12 @@ private GalacticCoord seatOf(long seed, long gx, long gy, long gz, double radius * 2d * Math.PI; LightYearVector offset = Galaxy.planeDirection(tilt, node, angle) .scale(-UniverseScale.HOME_GALAXY_ORIGIN_FRACTION * radiusLy); - return GalacticCoord.ofSectorLocal(UniverseScale.cellsAt(offset.x()), - UniverseScale.cellsAt(offset.y()), UniverseScale.cellsAt(offset.z()), + return GalacticCoord.ofSectorLocal(laws.cellsAt(offset.x()), + laws.cellsAt(offset.y()), laws.cellsAt(offset.z()), 0L, 0L, 0L); } long s = config.galaxySpacing; - long margin = Math.min(UniverseScale.cellsForLightYears(UniverseScale.retinueReachLy(radiusLy)), + long margin = Math.min(laws.cellsForLightYears(laws.retinueReachLy(radiusLy)), Math.max(0L, (s - 1L) / 2L)); long band = Math.max(1L, s - 2L * margin); // The index came from a real sector, so a cell corner is bounded by that sector and the diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index c8a4247b7..9e32bd951 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -291,8 +291,82 @@ public GalaxyGenConfig withRogueTuning(RogueTuning tuning) { * they name are folded in here rather than parsed twice. */ public GalaxyGenConfig withReservedGalaxies(List keys) { + // The rogue tuning is carried over EXPLICITLY. This runs after the catalogue walk, i.e. after + // has already been read, so going through the public constructor — which resets the + // unbound population to the measured default — would silently discard whatever the pack + // authored about rogues for every pack that also names a galaxy. return new GalaxyGenConfig(minSpacing, density, galaxySpacing, galaxyDensity, starTypes, - galaxyTypes, keys); + galaxyTypes, keys).withRogueTuning(rogue); + } + + /** + * A stable digest of every knob in this configuration — the identity of the universe these + * parameters describe. + * + *

    What it is FOR: a save records the fingerprint of the configuration it was generated under, + * and a later load compares. The generator is a pure function of {@code (seed, cell)} and these + * numbers, so a pack that retunes one of them is not tweaking balance — it is describing a + * different universe, in which every unpinned system moves. That is invisible without a stamp, and + * a moved system is discovered by a player arriving somewhere his notes do not match.

    + * + *

    Stable across JVMs and runs by construction: no {@link Object#hashCode()} anywhere (identity + * hashes and {@code String.hashCode} are not a promise across versions), doubles rendered through + * {@link Double#doubleToLongBits} rather than formatted (no locale, no rounding), and every list + * walked in its declared order — order IS part of the identity, because a weighted table's order + * decides which archetype a given hash lands on.

    + * + * @return 16 lowercase hex characters of SHA-256 over the canonical rendering — enough that a + * collision is not something a pack author will meet, short enough to read out of a log + */ + public String fingerprint() { + StringBuilder sb = new StringBuilder(512); + sb.append("v1;"); + sb.append("minSpacing=").append(minSpacing).append(';'); + sb.append("density=").append(bits(density)).append(';'); + sb.append("galaxySpacing=").append(galaxySpacing).append(';'); + sb.append("galaxyDensity=").append(bits(galaxyDensity)).append(';'); + for (StarType t : starTypes) { + sb.append("star[").append(t.temperature).append(',').append(bits(t.minSize)).append(',') + .append(bits(t.maxSize)).append(',').append(t.weight).append("];"); + } + for (GalaxyType t : galaxyTypes) { + sb.append("galaxy[").append(t.name).append(',').append(t.profile).append(',') + .append(bits(t.minRadiusLy)).append(',').append(bits(t.maxRadiusLy)).append(',') + .append(bits(t.scaleHeightRatio)).append(',').append(t.armCount).append(',') + .append(bits(t.rotationSpeedKmS)).append(',').append(bits(t.coreRadiusFraction)) + .append(',').append(t.minSatellites).append(',').append(t.maxSatellites) + .append(',').append(t.weight).append("];"); + } + for (ClusterType t : clusterTypes) { + sb.append("cluster[").append(t.name).append(',').append(t.subdivision).append(',') + .append(bits(t.minRadiusLy)).append(',').append(bits(t.maxRadiusLy)).append(',') + .append(bits(t.nebulaFraction)).append(',').append(t.selfBound).append(',') + .append(t.weight).append("];"); + } + for (GalaxyKey key : reservedGalaxies) { + sb.append("reserved[").append(key.gx()).append(',').append(key.gy()).append(',') + .append(key.gz()).append("];"); + } + sb.append("rogue[").append(bits(rogue.abundance)).append(',').append(bits(rogue.giantFraction)) + .append(',').append(bits(rogue.ejectaFalloff)).append(']'); + for (RogueType t : rogue.types) { + sb.append("rogueType[").append(t.name).append(',').append(t.primaryKind).append(',') + .append(t.weight).append("];"); + } + return digest(sb.toString()); + } + + /** The fingerprint of "no procedural generator at all" — an authored-anchors-only universe. */ + public static String noGeneratorFingerprint() { + return digest("none"); + } + + private static String bits(double v) { + return Fingerprint.bits(v); + } + + private static String digest(String canonical) { + return Fingerprint.hex16(canonical); } /** A sparse, strongly-clustered default galaxy. */ diff --git a/src/main/java/zmaster587/advancedRocketry/universe/IBodyDerivation.java b/src/main/java/zmaster587/advancedRocketry/universe/IBodyDerivation.java new file mode 100644 index 000000000..9375d0d26 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/IBodyDerivation.java @@ -0,0 +1,58 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.space.GalacticCoord; + +/** + * How a body's physics is drawn from its cell — the second half of a world model, and the half a + * player meets on the ground. + * + *

    Why this is an interface at all. A schema version is only worth having if an old save can + * still be derived the old way, and the derivation is exactly where a later version wants to move: new + * world types, a different mass law, another climate band. The generator seam alone could not carry + * that — it says WHERE things are, not WHAT they are. + * + *

    And why it costs nothing to thread. The derivation has one real consumer, the generator, + * which is already the object a schema hands out. So a version selects a derivation by selecting a + * generator, and everything else reaches it through {@link IGalaxyGenerator#derivation()} rather than + * through a static call that no version can intercept. + * + *

    Every method must be a pure, deterministic function of its arguments, for the same reason + * {@link IGalaxyGenerator}'s are: a scan and a later landing have to agree. + */ +public interface IBodyDerivation { + + /** The parent star's metal content relative to Sol, drawn once per system. */ + double metallicityOf(long seed, GalacticCoord anchor); + + /** The orbital distance a body of {@code star}'s system sits at, in AR distance units. */ + int referenceDistance(StellarBody star); + + /** Where body {@code index} of {@code count} sits around {@code star}. */ + int orbitalDistanceOf(long seed, GalacticCoord anchor, int index, int count, StellarBody star); + + /** The innermost orbit a body may hold around {@code star}. */ + double innerOrbit(StellarBody star); + + /** The outermost orbit a body may hold around {@code star}. */ + double outerOrbit(StellarBody star); + + /** The equilibrium temperature at {@code orbitalDistance}, before any atmosphere. */ + int bareTemperature(StellarBody star, int orbitalDistance); + + /** Whether a body at {@code orbitalDistance} keeps one face to its star. */ + boolean tidallyLockedAt(StellarBody star, int orbitalDistance); + + /** Whether body {@code index} accreted enough hydrogen to be a giant. */ + boolean isGiantAt(long seed, GalacticCoord anchor, int index, int bareTemperatureK); + + /** The full profile of a body BOUND to a star. */ + BodyProfile derive(long seed, GalacticCoord anchor, GalacticCoord bodyCell, int variant, + StellarBody star, boolean moon, int orbitalDistance); + + /** The full profile of an UNBOUND body — no star, no orbit, no insolation. */ + BodyProfile deriveRogue(long seed, GalacticCoord bodyCell, int variant, double giantFraction); + + /** What a body of this bulk still radiates with no star to warm it, in kelvin. */ + int residualTemperature(double massEarths, double radiusEarths); +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java index 4522703a6..da1e7fb7f 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java @@ -107,6 +107,45 @@ default int minSpacingCells() { return GalaxyGenConfig.DEFAULT_MIN_SPACING; } + /** + * The tunables this generator was built from, when it has any — what a {@code } element + * would have to say to reproduce it, and what the save fingerprints so a later load can tell that + * the pack has been retuned underneath it. + * + *

    Empty is a real answer and not a stub: a generator with no parameters (the authored-anchors-only + * default, or one an addon fabricates from something other than this config) has nothing to write + * back, and a pack file that carried a {@code } section for it would describe a generator + * nobody installed. + */ + default Optional tuning() { + return Optional.empty(); + } + + /** + * How this generator's bodies are derived — the half of a world model that says WHAT a body is, + * where {@link #systemAt} says where it is. + * + *

    It hangs here rather than on the schema because the generator is what a schema selects, so a + * version picks a derivation by picking a generator, and anything outside the universe layer that + * needs a body's physics asks the generator that produced the body. The default is version 1's, + * which is the right answer for a generator that does not derive anything of its own. + */ + default IBodyDerivation derivation() { + return BodyDerivationV0.INSTANCE; + } + + /** + * The metric and expansion this generator measures with — how many cells a light year is, and how + * the whole thing grows. + * + *

    Beside {@link #derivation()} and for the same reason: a schema selects the laws by selecting a + * generator, and anything outside this package that must convert a length in THIS world's terms + * asks the world's generator rather than a global. The default is version 1's. + */ + default IUniverseLaws laws() { + return UniverseLawsV0.INSTANCE; + } + /** * The cell an authored anchor declared against {@code key} is measured FROM, or empty when this * generator has no galaxies. diff --git a/src/main/java/zmaster587/advancedRocketry/universe/IUniverseLaws.java b/src/main/java/zmaster587/advancedRocketry/universe/IUniverseLaws.java new file mode 100644 index 000000000..1fd105695 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/IUniverseLaws.java @@ -0,0 +1,58 @@ +package zmaster587.advancedRocketry.universe; + +/** + * The METRIC and the EXPANSION — what a cell is worth in light years, and how the whole thing grows. + * + *

    One interface for both, because they are one thing. The expansion rate is expressed per + * tick, and a tick's worth of anything is a length, so {@code Cosmology}'s Hubble constant is derived + * through the metric's own conversion. Versioning them apart would let a build pair one release's + * metric with another's expansion, which is a universe neither of them describes. + * + *

    Why this is an instance and not the static class it forwards to. A released world model has + * to keep being derivable the way it was released, and a save re-derives everything untouched on every + * load. If the metric were global, a build that changed it would silently re-answer every existing + * world: an address a player wrote down would denote a different distance, with the same generator + * still running over it. Behind this seam the same build can hold a new metric for new worlds and the + * old one for the worlds that were made under it — which is the whole point of versioning the schema + * rather than the mod. + * + *

    What is deliberately NOT here. The lattice DEFAULTS ({@code DEFAULT_SPACING_CELLS}, + * {@code DEFAULT_GALAXY_SPACING_CELLS}) stay static: they only decide what a NEW world is given, and an + * existing world carries the numbers it was made with in its own {@code GalaxyGenConfig}. So do the + * drive-band constants, which price a machine rather than measure space — a rebalanced drive is a mod + * feature, and mod features are exactly what an old world is supposed to keep receiving. + * + *

    Implementations are pure and stateless: same arguments, same answer, for the life of the save. + */ +public interface IUniverseLaws { + + /** Cells spanned by {@code lightYears} — the chart metric, rounded down to whole cells. */ + long cellsForLightYears(double lightYears); + + /** The same conversion where a partial cell must not vanish (offsets rather than extents). */ + long cellsAt(double lightYears); + + /** What {@code cells} are worth in light years. */ + double lightYearsForCells(double cells); + + /** A speed quoted in km/s, in light years per tick. */ + double lightYearsPerTick(double kilometresPerSecond); + + /** Cells spanned by an orbital distance in Advanced Rocketry units. */ + long cellsForOrbitUnits(double orbitUnits); + + /** The inverse: what {@code cells} are worth in Advanced Rocketry orbital units. */ + double orbitUnitsForCells(long cells); + + /** The clear space a seat keeps inside a super-cell of {@code spacingCells}. */ + long seatMarginCells(long spacingCells); + + /** How far a primary's retinue reaches, given its radius in light years. */ + double retinueReachLy(double primaryRadiusLy); + + /** How much the universe has expanded by {@code tick}, as a factor on comoving distance. */ + double scaleFactorAt(long tick); + + /** The horizon a galaxy's peculiar drift is budgeted against, in ticks. */ + long driftHorizonTicks(); +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/LightYearVector.java b/src/main/java/zmaster587/advancedRocketry/universe/LightYearVector.java index 9db615d00..781e35c15 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/LightYearVector.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/LightYearVector.java @@ -32,11 +32,17 @@ public static LightYearVector of(double x, double y, double z) { } /** The position a cell NAME stands at in the static frame, in light years. */ - public static LightYearVector ofCell(GalacticCoord cell) { + /** + * A cell's position as a vector in light years, measured by {@code laws}. + * + *

    The metric is a PARAMETER because a cell is worth a different number of light years under a + * different schema, and this type is a plain value that must not decide which schema it belongs to. + */ + public static LightYearVector ofCell(GalacticCoord cell, IUniverseLaws laws) { return new LightYearVector( - UniverseScale.lightYearsForCells(cell.sectorX()), - UniverseScale.lightYearsForCells(cell.sectorY()), - UniverseScale.lightYearsForCells(cell.sectorZ())); + laws.lightYearsForCells(cell.sectorX()), + laws.lightYearsForCells(cell.sectorY()), + laws.lightYearsForCells(cell.sectorZ())); } public double x() { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java b/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java index 5727226bd..7b0b70426 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/Nebula.java @@ -81,6 +81,8 @@ public enum Appearance { REFLECTION } + /** The metric this object was seated under — its schema's, never a global one. */ + private final IUniverseLaws laws; private final StarCluster cluster; private final Appearance appearance; private final double centreXLy; @@ -90,7 +92,8 @@ public enum Appearance { private final double peakDensity; public Nebula(StarCluster cluster, Appearance appearance, double centreXLy, double centreYLy, - double centreZLy, double radiusLy, double peakDensity) { + double centreZLy, double radiusLy, double peakDensity, IUniverseLaws laws) { + this.laws = (laws == null) ? UniverseLawsV0.INSTANCE : laws; this.cluster = cluster; this.appearance = appearance; this.centreXLy = centreXLy; @@ -153,9 +156,9 @@ public double densityAt(double xLy, double yLy, double zLy) { /** The same reading at a cell name — the form the rest of the layer asks in. */ public double densityAtSector(long sectorX, long sectorY, long sectorZ) { - return densityAt(UniverseScale.lightYearsForCells(sectorX), - UniverseScale.lightYearsForCells(sectorY), - UniverseScale.lightYearsForCells(sectorZ)); + return densityAt(laws.lightYearsForCells(sectorX), + laws.lightYearsForCells(sectorY), + laws.lightYearsForCells(sectorZ)); } /** Whether a point is inside this nebula at all. */ diff --git a/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java b/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java index 0e81fe2b4..06f27bc8f 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/NebulaField.java @@ -36,9 +36,12 @@ public final class NebulaField { private static final int MAX_COLUMN_SAMPLES = 512; private final GalaxyGenConfig config; + /** The metric this field measures with — its schema's, not a global one. */ + private final IUniverseLaws laws; private final ClusterField clusters; - public NebulaField(GalaxyGenConfig config, ClusterField clusters) { + public NebulaField(GalaxyGenConfig config, ClusterField clusters, IUniverseLaws laws) { + this.laws = (laws == null) ? UniverseLawsV0.INSTANCE : laws; this.config = (config == null) ? GalaxyGenConfig.defaults() : config; this.clusters = clusters; } @@ -67,16 +70,16 @@ public Optional nebulaOf(long seed, StarCluster cluster) { double spreadRoll = CellHash.norm(CellHash.of(seed, cluster.centreSuperX(), cluster.centreSuperY(), cluster.centreSuperZ(), SALT_NEBULA_SPREAD)); - double clusterRadiusLy = UniverseScale.lightYearsForCells( + double clusterRadiusLy = laws.lightYearsForCells( (double) cluster.radiusSuperCells() * config.minSpacing); double radiusLy = clusterRadiusLy * Nebula.spreadFor(spreadRoll); long s = config.minSpacing; return Optional.of(new Nebula(cluster, Nebula.appearanceFor(gas), - UniverseScale.lightYearsForCells((double) cluster.centreSuperX() * s), - UniverseScale.lightYearsForCells((double) cluster.centreSuperY() * s), - UniverseScale.lightYearsForCells((double) cluster.centreSuperZ() * s), - radiusLy, gas)); + laws.lightYearsForCells((double) cluster.centreSuperX() * s), + laws.lightYearsForCells((double) cluster.centreSuperY() * s), + laws.lightYearsForCells((double) cluster.centreSuperZ() * s), + radiusLy, gas, laws)); } /** The cloud covering this coarse super-cell, if a cluster covers it and still has one. */ @@ -151,12 +154,12 @@ public double columnDensityBetween(long seed, Galaxy galaxy, GalacticCoord from, } GalacticCoord a = from.cellCentre(); GalacticCoord b = to.cellCentre(); - double ax = UniverseScale.lightYearsForCells(a.sectorX()); - double ay = UniverseScale.lightYearsForCells(a.sectorY()); - double az = UniverseScale.lightYearsForCells(a.sectorZ()); - double bx = UniverseScale.lightYearsForCells(b.sectorX()); - double by = UniverseScale.lightYearsForCells(b.sectorY()); - double bz = UniverseScale.lightYearsForCells(b.sectorZ()); + double ax = laws.lightYearsForCells(a.sectorX()); + double ay = laws.lightYearsForCells(a.sectorY()); + double az = laws.lightYearsForCells(a.sectorZ()); + double bx = laws.lightYearsForCells(b.sectorX()); + double by = laws.lightYearsForCells(b.sectorY()); + double bz = laws.lightYearsForCells(b.sectorZ()); double dx = bx - ax, dy = by - ay, dz = bz - az; double lengthLy = Math.sqrt(dx * dx + dy * dy + dz * dz); if (lengthLy <= 0d) { @@ -180,9 +183,9 @@ public double columnDensityBetween(long seed, Galaxy galaxy, GalacticCoord from, /** The density at a point stated in light years — what the line integral samples. */ public double densityAtLightYears(long seed, Galaxy galaxy, double xLy, double yLy, double zLy) { long s = config.minSpacing; - long sectorX = UniverseScale.cellsAt(xLy); - long sectorY = UniverseScale.cellsAt(yLy); - long sectorZ = UniverseScale.cellsAt(zLy); + long sectorX = laws.cellsAt(xLy); + long sectorY = laws.cellsAt(yLy); + long sectorZ = laws.cellsAt(zLy); Optional nebula = nebulaAt(seed, galaxy, Math.floorDiv(sectorX, s), Math.floorDiv(sectorY, s), Math.floorDiv(sectorZ, s)); return nebula.isPresent() ? nebula.get().densityAt(xLy, yLy, zLy) : 0d; diff --git a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java index e06f108d8..e90aba161 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/PlanetRealizer.java @@ -142,7 +142,8 @@ public static int realize(MinecraftServer server, GalacticCoord bodyCell) { return Constants.INVALID_PLANET; } - BodyProfile profile = PlanetDerivation.derive(registry.worldSeed(), anchor, target.name(), variant, + BodyProfile profile = UniverseRegistry.getGenerator().derivation() + .derive(registry.worldSeed(), anchor, target.name(), variant, star, target.kind() == SystemBodyKind.MOON, target.orbitalDistance()); DimensionProperties props = materialize(dimId, profile, star, target, parentBody); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java b/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java index 48f245b01..5d623d417 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java @@ -174,7 +174,8 @@ public static RegionScan local(GalacticCoord origin, int radiusCells, long start */ private static int stepTicks(long distanceCells, Tuning tuning) { double ticks = tuning.baseTicks() - + tuning.ticksPerLightYear() * UniverseScale.lightYearsForCells(distanceCells); + + tuning.ticksPerLightYear() + * UniverseRegistry.getGenerator().laws().lightYearsForCells(distanceCells); return (int) Math.max(0L, Math.min(Integer.MAX_VALUE, Math.round(ticks))); } @@ -195,7 +196,7 @@ public long distanceCells() { /** The same reach in light years — the form the number is recognisable in. */ public double distanceLightYears() { - return UniverseScale.lightYearsForCells(distanceCells); + return UniverseRegistry.getGenerator().laws().lightYearsForCells(distanceCells); } /** How far apart the cells this survey looks at stand. One star's territory, or one cell. */ @@ -406,7 +407,8 @@ public long strideCells() { /** The horizon as a number of steps, which is what an operator aims in. At least one. */ public int maxRangeSteps() { - long steps = UniverseScale.cellsForLightYears(maxRangeLightYears) / strideCells; + long steps = UniverseRegistry.getGenerator().laws() + .cellsForLightYears(maxRangeLightYears) / strideCells; return (int) Math.max(1L, Math.min(Integer.MAX_VALUE, steps)); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java index a029b3a72..530adddef 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java @@ -151,6 +151,19 @@ public static int resolveCell(UniverseRegistry registry, GalacticCoord cell, Cry if (!anchor.isPresent()) { return 0; } + // A LOOK IS A TOUCH. Everything below hands the operator something durable — an address he can + // fly to, a body he can name — out of a derivation that a later seed, config or generator edit + // would answer differently. Pinning first freezes the system into the save before a word of it + // is written down, so what the crystal holds and what the sky holds cannot come apart. + // + // The unit is the whole SYSTEM and not the bodies enumerated, because a system is what a pin + // can key: an obscured look still yields the address and the primary kind, and those are the + // system's identity. Freezing bodies the operator has not resolved yet is the conservative + // direction — they are what he will find when he gets there. + // + // Idempotent and free for anything already authored or pinned, so a re-scan of known sky and + // the many member cells of one system cost one pin between them. + registry.pinSystem(anchor.get()); int written = 0; boolean namedSomething = false; if (!isObscured(registry, observer, anchor.get())) { diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseLawsV0.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseLawsV0.java new file mode 100644 index 000000000..cbc27dfa6 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseLawsV0.java @@ -0,0 +1,71 @@ +package zmaster587.advancedRocketry.universe; + +/** + * Schema version 0's metric and expansion — every number exactly as {@link UniverseScale} and + * {@link Cosmology} state it. + * + *

    A pure forwarder, for the same reason {@link BodyDerivationV0} is one: the arithmetic stays where + * its constants are documented next to the observations they come from, and this class is only the + * handle a schema holds it by. A version 2 is a second implementation, never an edit to those two + * classes — editing them in place would change the universe under every world already made, which is + * what {@code universeLawsFingerprint} exists to catch. + * + *

    Stateless, so one instance serves every world. + */ +public final class UniverseLawsV0 implements IUniverseLaws { + + public static final UniverseLawsV0 INSTANCE = new UniverseLawsV0(); + + private UniverseLawsV0() { + } + + @Override + public long cellsForLightYears(double lightYears) { + return UniverseScale.cellsForLightYears(lightYears); + } + + @Override + public long cellsAt(double lightYears) { + return UniverseScale.cellsAt(lightYears); + } + + @Override + public double lightYearsForCells(double cells) { + return UniverseScale.lightYearsForCells(cells); + } + + @Override + public double lightYearsPerTick(double kilometresPerSecond) { + return UniverseScale.lightYearsPerTick(kilometresPerSecond); + } + + @Override + public long cellsForOrbitUnits(double orbitUnits) { + return UniverseScale.cellsForOrbitUnits(orbitUnits); + } + + @Override + public double orbitUnitsForCells(long cells) { + return UniverseScale.orbitUnitsForCells(cells); + } + + @Override + public long seatMarginCells(long spacingCells) { + return UniverseScale.seatMarginCells(spacingCells); + } + + @Override + public double retinueReachLy(double primaryRadiusLy) { + return UniverseScale.retinueReachLy(primaryRadiusLy); + } + + @Override + public double scaleFactorAt(long tick) { + return Cosmology.scaleFactorAt(tick); + } + + @Override + public long driftHorizonTicks() { + return Cosmology.DRIFT_HORIZON_TICKS; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java index 20a75e80e..418653c9c 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java @@ -58,7 +58,16 @@ public final class UniverseRegistry extends WorldSavedData implements CellFrames public static final String STORAGE_KEY = "advancedrocketry_universe"; // v3: + durable cell names (derived once, then persisted and never re-derived) and their owning system. - private static final int NBT_VERSION = 3; + // v4: + the world-model stamp (schema version + galaxy-config fingerprint). + private static final int NBT_VERSION = 4; + + /** + * A save with no world-model stamp: a fresh world, or one written before the stamp existed. + * + *

    Negative on purpose. Version numbers start at ZERO — the alpha — so a sentinel of 0 would have + * read every alpha world as unstamped and silently re-adopted whatever the build shipped. + */ + public static final int UNSTAMPED = -1; // A self-contained logger rather than AdvancedRocketry.logger: loading the mod class triggers Forge // bootstrap (FluidRegistry.enableUniversalBucket), which would break pure unit tests of this registry. @@ -91,6 +100,45 @@ public final class UniverseRegistry extends WorldSavedData implements CellFrames private final Map namesByDim = new HashMap<>(); /** Latch: authored anchors drain into the store exactly once (unless a config XML reset is forced). */ private boolean anchorsSeeded = false; + /** + * The world model this save was generated under — {@link UniverseSchema#version()}, or + * {@link #UNSTAMPED} for a world made before the stamp existed. + * + *

    Deliberately SEPARATE from {@link #NBT_VERSION}: that one is the layout of these tags and + * moves whenever a field is added here, while this one is the identity of the universe those tags + * describe. A save whose tag layout is a version behind still describes the same sky; a save whose + * schema is a version behind describes a different one. + */ + private int schemaVersion = UNSTAMPED; + /** + * The fingerprint of the {@code } configuration this save was generated under. The + * schema decides HOW space is derived; these knobs decide the particular universe it derives, and + * an edit to either one moves every system nobody has touched yet. + */ + private String configFingerprint = ""; + /** + * The fingerprint of the LAWS this save was generated under — the metric and the expansion. + * + *

    Kept apart from the configuration's because the two are edited by different people for + * different reasons: the configuration is the pack author's, the laws are the mod's. Sharing one + * stamp would let a refusal blame the wrong one, and the remedies are not the same. + */ + private String lawsFingerprint = ""; + /** + * Set by an operator's upgrade, consumed by the NEXT load: permission, given once, to accept a + * {@code } that has changed. + * + *

    It exists because the refusal and its remedy cannot both live inside a running server. A + * fingerprint is one-way, so a save whose configuration has changed cannot be opened under the + * universe it was made with — the load has to refuse — and a command inside a server that refuses + * to start cannot be typed. So the acceptance is armed while the world still loads, which is also + * the only moment crystals can be read and the systems on them frozen, and it is spent at the boot + * after. + * + *

    Consumed exactly once, and only when the configuration actually differs, so an accidental + * edit a year later is refused like any other. + */ + private boolean upgradeArmed = false; // ─── Transient, re-derived per load ─────────────────────────────────────── /** The world seed fed to the generator; set by {@link #bindWorldSeed}, never persisted. */ @@ -109,6 +157,13 @@ public final class UniverseRegistry extends WorldSavedData implements CellFrames // supply fabricated systems. private static volatile IntFunction starLookup = UniverseRegistry::lookupCatalogueStar; private static Map pendingAnchors = new HashMap<>(); + /** + * The pack's {@code } configuration for this session, staged while dimensions load and + * paired with the save's schema stamp at {@link #populate}. Null means the pack declares none. + */ + private static volatile GalaxyGenConfig packGalaxyConfig = null; + /** The model {@link #populate} put in force for this session; null until it has run. */ + private static volatile UniverseSchema activeSchema = null; private static boolean pendingReset = false; /** @@ -1011,6 +1066,212 @@ public long worldSeed() { return worldSeed; } + // ─── The world-model stamp (schema version + config fingerprint) ─────────── + + /** The world model this save was generated under, or {@link #UNSTAMPED}. */ + public int schemaVersion() { + return schemaVersion; + } + + /** + * The world model in force, or empty before {@link #populate} has resolved one. + * + *

    What a caller usually wants this for is {@link UniverseSchema#isStable()} — whether the world + * it is about to touch was generated by an ALPHA model that may be replaced rather than carried + * forward. + */ + public static Optional activeSchema() { + return Optional.ofNullable(activeSchema); + } + + /** How many procedural systems this save has frozen — what an upgrade would carry over untouched. */ + public int pinnedSystemCount() { + return pinnedSystems.size(); + } + + /** The galaxy-config fingerprint this save was generated under; empty when unstamped. */ + public String configFingerprint() { + return configFingerprint; + } + + /** The laws fingerprint (metric + expansion) this save was generated under; empty when unstamped. */ + public String lawsFingerprint() { + return lawsFingerprint; + } + + /** + * The identity of a set of laws, taken by MEASURING them rather than by listing their constants. + * + *

    Fixed inputs through every conversion, plus the expansion at fixed ticks, hashed. Two reasons + * it is done this way. It works for any implementation, so a schema version 2 with its own metric + * needs no fingerprinting code of its own. And it catches what a declaration cannot: an + * implementation whose internal constant moved while whatever list it publishes stayed the same. + */ + public static String lawsFingerprintOf(IUniverseLaws laws) { + StringBuilder sb = new StringBuilder(256); + sb.append("laws1;"); + double[] lightYears = {0.1d, 1d, 4.23d, 100d, 50_000d}; + for (double ly : lightYears) { + sb.append(laws.cellsForLightYears(ly)).append(',').append(laws.cellsAt(ly)).append(';'); + } + long[] cells = {1L, 1_000_000L, 5_002_361L}; + for (long c : cells) { + sb.append(Fingerprint.bits(laws.lightYearsForCells(c))).append(',') + .append(Fingerprint.bits(laws.orbitUnitsForCells(c))).append(',') + .append(laws.seatMarginCells(c)).append(';'); + } + sb.append(Fingerprint.bits(laws.lightYearsPerTick(1d))).append(';'); + sb.append(laws.cellsForOrbitUnits(1d)).append(';'); + sb.append(Fingerprint.bits(laws.retinueReachLy(1d))).append(';'); + for (long tick : new long[]{0L, 24_000L, 24_000_000L}) { + sb.append(Fingerprint.bits(laws.scaleFactorAt(tick))).append(';'); + } + sb.append(laws.driftHorizonTicks()); + return Fingerprint.hex16(sb.toString()); + } + + /** What THIS build's newest schema measures with — what a fresh world is stamped against. */ + public static String currentLawsFingerprint() { + return lawsFingerprintOf(UniverseSchemas.current().laws()); + } + + /** + * Decide which world model this save must be read under, and stamp it if it has none yet. + * + *

    The version comes from the SAVE, never from the pack. That inversion is the whole + * mechanism: a world generated under schema 1 keeps being derived by schema 1 after the mod ships + * schema 2, so the mod is free to move — new mechanics, new blocks, new balance — while the sky a + * player has already charted stays where he charted it. Only {@code upgrade} moves a world. + * + *

    Two refusals, and both are recoverable from outside the game. A stamp naming a version + * this build does not carry (a world from a newer jar, or one whose version was dropped) and a + * configuration that has been edited since the world was made. Neither can be honoured by + * substituting something close: continuing would answer a different universe under an unchanged + * save, and the player would find out by flying somewhere his notes describe. + * + *

    Note that a pack edit which merely ADDS — one more authored anchor naming a new galaxy, one + * more star archetype — changes the fingerprint like any other, and that is correct rather than + * strict: a reserved galaxy is a galaxy forced into a cell that had its own contents, and one more + * weight moves every draw that walks the table. + * + * @param config the pack's {@code } configuration, or {@code null} for an + * authored-anchors-only universe + * @return the schema to install for this world + * @throws UniverseSchemaMismatchException when the save cannot be honoured by this build + */ + public UniverseSchema reconcileSchema(GalaxyGenConfig config) { + String fingerprint = fingerprintOf(config); + if (schemaVersion == UNSTAMPED) { + UniverseSchema schema = UniverseSchemas.current(); + if (!byCell.isEmpty() || !pinnedSystems.isEmpty()) { + // A world with content but no stamp predates the stamp. Nothing records what generated + // it, so adopting the current model is the only move available — said out loud, because + // it is the one case where this class cannot prove the sky is unchanged. + LOGGER.warn("Universe save carries content but no world-model stamp; adopting schema {} " + + "and configuration {}. If this world was generated by a different build, its " + + "untouched systems may have moved.", schema.version(), fingerprint); + } + stampSchema(schema.version(), fingerprint); + return schema; + } + Optional saved = UniverseSchemas.of(schemaVersion); + if (!saved.isPresent()) { + throw new UniverseSchemaMismatchException( + "This world was generated under universe schema " + schemaVersion + + ", which this build does not carry (it has " + UniverseSchemas.released() + + "). Install a build that carries schema " + schemaVersion + + " to open this world."); + } + // Measured against the laws of the schema THIS SAVE is owed, not the newest ones. A build that + // ships a new metric ships it as a new schema version, and this world simply keeps using its own + // — which is why a mismatch here does not mean "the mod moved on". It means schema + // %d's laws in this jar are not the ones that made this world, i.e. a released version was + // edited in place. That is a developer error, and there is nothing a player or an operator can + // do about it, so it is not something an upgrade may accept. + String laws = lawsFingerprintOf(saved.get().laws()); + if (!lawsFingerprint.isEmpty() && !lawsFingerprint.equals(laws)) { + throw new UniverseSchemaMismatchException( + "Universe schema " + schemaVersion + " in this build does not measure the way it did " + + "when this world was generated: the world was made under laws " + + lawsFingerprint + " and this build's schema " + schemaVersion + " states " + + laws + ". A released schema's metric and expansion may never change — a " + + "changed metric ships as a NEW schema version, which old worlds simply do " + + "not use. This build is broken; install one whose schema " + schemaVersion + + " is intact."); + } + if (!configFingerprint.equals(fingerprint)) { + if (upgradeArmed) { + // Permission was given last session, by an operator, on a world that was still loading + // — which is when the crystals could be read and their systems frozen. Spend it. + LOGGER.warn("Accepting the changed for this world: {} -> {}. This was armed " + + "by an operator's upgrade. Systems already frozen keep exactly what they held; " + + "everything else is re-derived from here.", configFingerprint, fingerprint); + upgradeArmed = false; + stampSchema(UniverseSchemas.CURRENT, fingerprint); + return UniverseSchemas.current(); + } + throw new UniverseSchemaMismatchException( + "The configuration has changed since this world was generated: it was " + + "made under " + configFingerprint + " and this pack states " + fingerprint + + ". Every system nobody has visited yet would move, so this world will not " + + "open under it. " + + "To go back: restore the previous and start again. " + + "To accept the change: restore the previous , start, run " + + "\"/stellurgy universe upgrade confirm\" (that freezes every system anyone " + + "has seen, including the addresses on the memory crystals of players who " + + "are online), stop, put the new configuration back, and start again."); + } + return saved.get(); + } + + /** + * Accept {@code config} (and the current schema) as this world's model from now on — the write half + * of the upgrade, after everything already seen has been pinned. + * + * @return the schema now in force + */ + /** + * Whether this world is holding an operator's one-shot permission to accept a changed + * {@code } at its next load. + */ + public boolean isUpgradeArmed() { + return upgradeArmed; + } + + /** + * Give that permission — the half of an upgrade that a running server can perform for a change it + * cannot see yet. It is spent by the next load, and only if the configuration has actually moved. + */ + public void armUpgrade() { + if (!upgradeArmed) { + upgradeArmed = true; + markDirty(); + } + } + + public UniverseSchema adoptSchema(GalaxyGenConfig config) { + UniverseSchema schema = UniverseSchemas.current(); + stampSchema(schema.version(), fingerprintOf(config)); + return schema; + } + + /** The fingerprint a {@code null} (authored-anchors-only) configuration has its own name for. */ + public static String fingerprintOf(GalaxyGenConfig config) { + return (config == null) ? GalaxyGenConfig.noGeneratorFingerprint() : config.fingerprint(); + } + + private void stampSchema(int version, String fingerprint) { + String laws = currentLawsFingerprint(); + if (schemaVersion == version && configFingerprint.equals(fingerprint) + && lawsFingerprint.equals(laws)) { + return; + } + schemaVersion = version; + configFingerprint = fingerprint; + lawsFingerprint = laws; + markDirty(); + } + // ─── Static staging + population (server lifecycle) ──────────────────────── /** @@ -1022,6 +1283,26 @@ public static void stageAnchors(Map anchors, boolean re pendingReset = reset; } + /** + * Hand over the pack's {@code } configuration, read while dimensions load — before the + * save is reachable, so before anything can know which model this world is owed. + * + *

    The pack states the KNOBS; the save states the VERSION. {@link #populate} puts the two together + * and installs the generator, which is why the generator is no longer built at the XML site: doing + * it there would make the pack the authority on a question that belongs to the world. + * + *

    It is kept for the session rather than drained, because an upgrade run later needs the same + * configuration to stamp. + */ + public static void stageGalaxyConfig(GalaxyGenConfig config) { + packGalaxyConfig = config; + } + + /** The pack's {@code } configuration for this session, or {@code null} if it declares none. */ + public static GalaxyGenConfig packGalaxyConfig() { + return packGalaxyConfig; + } + /** * Server-start hook (call once worlds are loaded): bind the world seed, drain staged anchors, and give * every remaining catalogued star a fallback coord. Idempotent across restarts. @@ -1038,6 +1319,23 @@ public static void populate(MinecraftServer server) { if (overworld != null) { reg.bindWorldSeed(overworld.getSeed()); } + // Raise the world model from the SAVE and install its generator, BEFORE anything derives. + // applyAnchors resolves declared positions through the generator, so an anchor placed under the + // wrong model would be placed wrongly and then persisted. + UniverseSchema schema = reg.reconcileSchema(packGalaxyConfig); + activeSchema = schema; + setGenerator(schema.generator(packGalaxyConfig)); + LOGGER.info("Universe schema {} ({}) in force, configuration {}", schema.version(), + schema.label(), reg.configFingerprint()); + if (!schema.isStable()) { + // Loud, and at WARN, because it is a statement about the FUTURE of this save rather than + // about anything wrong with it now: an alpha model may be replaced outright, and a world + // built on one is not promised a way forward. + LOGGER.warn("Universe generator {} is an ALPHA. Its leading zero means the world model may " + + "be REPLACED in a later release rather than extended: worlds generated under it " + + "are not guaranteed to be carried forward, and only what has already been seen is " + + "frozen. Do not start a world you intend to keep for years on it.", schema.label()); + } reg.applyAnchors(pendingAnchors, pendingReset); reg.assignFallbackCoords(DimensionManager.getInstance().getStars()); pendingAnchors = new HashMap<>(); @@ -1106,6 +1404,14 @@ public void readFromNBT(NBTTagCompound nbt) { namesByDim.clear(); anchorsBySuper = null; anchorsSeeded = nbt.getBoolean("anchorsSeeded"); + // Read through hasKey, NEVER through the value alone. NBT answers 0 for an absent integer, and + // 0 is a real version number — the alpha — so taking the default would report every stampless + // save as "generated by the alpha" and quietly skip the adoption that a fresh world is owed. + // This is also why UNSTAMPED is negative: no version is. + schemaVersion = nbt.hasKey("schemaVersion") ? nbt.getInteger("schemaVersion") : UNSTAMPED; + configFingerprint = nbt.getString("galaxyConfigFingerprint"); + lawsFingerprint = nbt.getString("universeLawsFingerprint"); + upgradeArmed = nbt.getBoolean("universeUpgradeArmed"); NBTTagList names = nbt.getTagList("cellNames", 10 /* NBTTagCompound */); for (int i = 0; i < names.tagCount(); i++) { NBTTagCompound e = names.getCompoundTagAt(i); @@ -1157,6 +1463,10 @@ public void readFromNBT(NBTTagCompound nbt) { public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt.setInteger("version", NBT_VERSION); nbt.setBoolean("anchorsSeeded", anchorsSeeded); + nbt.setInteger("schemaVersion", schemaVersion); + nbt.setString("galaxyConfigFingerprint", configFingerprint); + nbt.setString("universeLawsFingerprint", lawsFingerprint); + nbt.setBoolean("universeUpgradeArmed", upgradeArmed); NBTTagList list = new NBTTagList(); for (Map.Entry e : byStar.entrySet()) { NBTTagCompound entry = new NBTTagCompound(); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchema.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchema.java new file mode 100644 index 000000000..cdc6a7c42 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchema.java @@ -0,0 +1,99 @@ +package zmaster587.advancedRocketry.universe; + +/** + * One released version of the WORLD MODEL — the thing a save is generated under and must keep being + * read under for the life of that save. + * + *

    What is inside the version number. The schema is not the generator alone; it is + * {@link IGalaxyGenerator} + {@link PlanetDerivation} + {@link UniverseScale} + {@link Cosmology} + * together. Everything a telescope PROMISES versions as one unit: an address is worth nothing if the + * derivation that gives that address a radius, a temperature and an orbit has moved underneath it, and + * a light year is worth nothing if the metric that converts it to cells has. + * + *

    All four are versioned the same way: by implementation. A version hands out a generator, + * and the generator carries the other two — {@link IGalaxyGenerator#derivation()} and + * {@link IGalaxyGenerator#laws()} — so selecting a version selects all of it. Nothing about a released + * world model reads a global, which is what lets one build hold a new model for new worlds and the old + * one for the worlds already made under it. That is the whole purpose: the mod moves on, and a world + * does not have to. + * + *

    What stays global, and why that is not a hole. The lattice DEFAULTS + * ({@code DEFAULT_SPACING_CELLS} and friends) decide only what a NEW world is given — an existing one + * carries its own numbers in its {@code GalaxyGenConfig}. The drive-band constants price a machine + * rather than measure space, and a rebalanced drive is a mod feature, which is exactly what an old + * world is supposed to keep receiving. + * + *

    The stamp that remains is a tripwire, not a barrier. + * {@code UniverseRegistry.lawsFingerprintOf} measures a version's laws — fixed inputs through every + * conversion — and the save records what its own version measured when the world was made. A mismatch + * therefore no longer means "the mod moved on"; it means a RELEASED version was edited in place, which + * is a developer error nobody downstream can accept away. + * + *

    All of it is backed by one mechanical check: the golden corpus renders what the whole chain + * produces — placement, derivation, metric and expansion — and compares it byte for byte, so a change + * in any of the four turns a test red and forces the version decision rather than shipping as a + * surprise. + * + *

    How a new version is written. As a DECORATOR over the one before it, delegating everything + * it does not deliberately change: + * + *

    + * final class UniverseSchemaV2 implements UniverseSchema {
    + *     private final UniverseSchema previous = new UniverseSchemaV0();
    + *     public int version() { return 2; }
    + *     public IGalaxyGenerator generator(GalaxyGenConfig config) { ...the one thing that changed... }
    + * }
    + * 
    + * + *

    Delegation rather than a fresh implementation is what makes the invariants inherited instead of + * re-typed: a v2 that only re-prices rogues has one method of its own, and every other guarantee is + * still v1's code rather than a copy of it that will drift. + * + *

    Implementations are immutable and hold no world state. A schema may be instantiated many + * times, and two instances of the same version must be indistinguishable. + */ +public interface UniverseSchema { + + /** + * The released version number. Stamped into the save and used to find this schema again when that + * save is opened by a later build. Never reused, never renumbered — it is an identifier, like an + * NBT key. + */ + int version(); + + /** + * The human name of this version — {@code MAJOR.MINOR}, and the MAJOR is a promise. + * + *

    A leading zero means ALPHA: the model may be replaced outright rather than extended, and + * nothing about it is guaranteed to survive to the next release. That is not a disclaimer, it is + * the whole meaning of the digit, and players are told so on the world where it applies. + * + *

    It is a separate thing from {@link #version()} because the two answer different questions. The + * number is the world's IDENTITY: it is stamped into saves, keys the registry, and may never be + * reused or reordered. The label is a STATEMENT ABOUT MATURITY, and several successive alphas can + * be shipped — {@code "0.1"}, {@code "0.2"} — each with its own identity, none of them stable. + */ + String label(); + + /** + * Whether this version is a stable release. False for anything whose {@link #label()} begins with + * {@code 0.} — an alpha, which a player is warned about and which may be replaced rather than + * carried forward. + */ + default boolean isStable() { + return !label().startsWith("0."); + } + + /** + * The generator this schema produces for {@code config}, or the empty generator when the pack + * declares no {@code } (an authored-anchors-only universe, which is a legitimate world + * rather than a missing configuration). + */ + IGalaxyGenerator generator(GalaxyGenConfig config); + + /** + * The metric and expansion this version measures with. One source: the generator this schema builds + * is handed this very instance, so the two can never describe different universes. + */ + IUniverseLaws laws(); +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemaMismatchException.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemaMismatchException.java new file mode 100644 index 000000000..4de71a4f3 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemaMismatchException.java @@ -0,0 +1,22 @@ +package zmaster587.advancedRocketry.universe; + +/** + * Thrown when a save cannot be opened under the world model this build would give it — a schema version + * this jar does not carry, or a {@code } configuration that has been edited since the world + * was made. + * + *

    Why this is fatal rather than a warning. The universe is derived, not stored: a save keeps + * what has been touched and re-derives everything else from {@code (seed, cell)}. Continuing under a + * different model does not corrupt the file — it quietly answers a DIFFERENT universe, and the player + * finds out by flying to an address he wrote down and finding nothing there. A refusal to load is + * recoverable from outside the game (restore the configuration, or install the build that carries the + * version); a world silently regenerated around the player's notes is not. + */ +public class UniverseSchemaMismatchException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public UniverseSchemaMismatchException(String message) { + super(message); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemaV0.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemaV0.java new file mode 100644 index 000000000..d384f4aaa --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemaV0.java @@ -0,0 +1,44 @@ +package zmaster587.advancedRocketry.universe; + +/** + * Schema version 0 ("0.1", the ALPHA) — the clustered galaxy field as first released: nested galaxy and star lattices, + * cluster sub-lattices, seated nebulae, the unbound population out in the void, and the body + * derivation those systems are filled with. + * + *

    Deliberately thin. A schema version exists to be NAMED and found again, not to hold logic; the + * behaviour lives in the classes it selects, and this class is the record that this particular set of + * them was once shipped. + * + *

    The zero is a promise about maturity, not a placeholder. This model may be replaced outright + * in a later release rather than extended, so a world generated under it is not guaranteed a future — + * and the player is told exactly that when he loads one. + */ +public final class UniverseSchemaV0 implements UniverseSchema { + + public static final int VERSION = 0; + + /** The alpha, and its leading zero says so. */ + public static final String LABEL = "0.1"; + + @Override + public int version() { + return VERSION; + } + + @Override + public String label() { + return LABEL; + } + + @Override + public IUniverseLaws laws() { + return UniverseLawsV0.INSTANCE; + } + + @Override + public IGalaxyGenerator generator(GalaxyGenConfig config) { + return (config == null) + ? new EmptyGalaxyGenerator() + : new ClusteredGalaxyGenerator(config, BodyDerivationV0.INSTANCE, laws()); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemas.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemas.java new file mode 100644 index 000000000..d8fa5b3a3 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseSchemas.java @@ -0,0 +1,75 @@ +package zmaster587.advancedRocketry.universe; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Every released world model this build can still speak, keyed by version. + * + *

    The jar carries all of them, and that is the point. A save stamped with version n is + * opened under version n forever, whatever the mod has moved on to — so the mod can be updated + * freely (mechanics, blocks, balance, whole new machines) without the sky changing under a world that + * has already been explored. Only the player's explicit upgrade moves a world onto a newer model. + * + *

    A RELEASED version is added here and never removed: dropping one makes every save carrying its + * stamp unopenable. A version that has not shipped is a different matter — it may be edited in place + * and even replaced outright, because no world outside the branch was ever generated under it and it + * therefore owes nobody compatibility. "Shipped" means merged to the release branch, not landed + * on a feature branch; the freeze begins at the merge, and that is the moment a version stops being + * editable and starts being history. + * + *

    Registering a supplier rather than an instance keeps construction lazy and makes it explicit that + * a schema is cheap to build and holds no world state. + */ +public final class UniverseSchemas { + + private static final Map> REGISTRY = + new LinkedHashMap>(); + + static { + register(UniverseSchemaV0.VERSION, new Supplier() { + @Override + public UniverseSchema get() { + return new UniverseSchemaV0(); + } + }); + } + + /** The newest released version — what a fresh world is stamped with. */ + public static final int CURRENT = UniverseSchemaV0.VERSION; + + private UniverseSchemas() { + } + + private static void register(int version, Supplier supplier) { + REGISTRY.put(version, supplier); + } + + /** The schema for {@code version}, or empty when this build does not carry it. */ + public static Optional of(int version) { + Supplier supplier = REGISTRY.get(version); + return (supplier == null) ? Optional.empty() : Optional.of(supplier.get()); + } + + /** The newest released schema — what a world with no stamp of its own is generated under. */ + public static UniverseSchema current() { + Optional schema = of(CURRENT); + if (!schema.isPresent()) { + throw new IllegalStateException("the current universe schema " + CURRENT + + " is not registered"); + } + return schema.get(); + } + + /** Every version this build carries, ascending — for diagnostics and for the refusal message. */ + public static List released() { + List versions = new ArrayList<>(REGISTRY.keySet()); + Collections.sort(versions); + return Collections.unmodifiableList(versions); + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java index 73937f0dd..e30f09f4f 100644 --- a/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java +++ b/src/main/java/zmaster587/advancedRocketry/util/XMLPlanetLoader.java @@ -717,8 +717,10 @@ public static String writeXML(IGalaxy galaxy) { // Emit the active procedural generator's config so a re-read (resetFromXml) round-trips it. IGalaxyGenerator activeGenerator = UniverseRegistry.getGenerator(); - if (activeGenerator instanceof ClusteredGalaxyGenerator) { - galaxyElement.appendChild(writeGalaxyGen(doc, ((ClusteredGalaxyGenerator) activeGenerator).config())); + java.util.Optional tuning = + activeGenerator.tuning(); + if (tuning.isPresent()) { + galaxyElement.appendChild(writeGalaxyGen(doc, tuning.get())); // The planet-type table travels with the generator, and only with it: an authored-anchors-only // world has nothing that draws a type, so writing the presets there would put a section into // the file that nothing reads. diff --git a/src/main/resources/assets/advancedrocketry/lang/en_US.lang b/src/main/resources/assets/advancedrocketry/lang/en_US.lang index 8a7599eb1..1fa4acca4 100644 --- a/src/main/resources/assets/advancedrocketry/lang/en_US.lang +++ b/src/main/resources/assets/advancedrocketry/lang/en_US.lang @@ -317,6 +317,27 @@ commands.advancedrocketry.dev.dumpbiomes.usage=dumpBiomes - Dumps biome info to commands.advancedrocketry.dev.dumpbiomes.success=The file 'BiomeDump.txt' has been written to the instance directory commands.advancedrocketry.dev.runtests.usage=runTests - Runs rocket tests for debug only! +msg.advancedrocketry.universe.alpha=This world uses universe generator %s - an ALPHA. The leading zero means the world model may be replaced in a later release rather than extended, so this world is not guaranteed a way forward. Only what you have already seen is frozen. +commands.advancedrocketry.universe.usage=/stellurgy universe help - the world model this save was generated under +commands.advancedrocketry.universe.unavailable=The universe registry is not available on this world +commands.advancedrocketry.universe.status.usage=status - report the world model, the pack's configuration, and how much sky is frozen +commands.advancedrocketry.universe.status.schema=World model: schema %s (this build ships %s) +commands.advancedrocketry.universe.status.alpha=Generator %s is an ALPHA: a leading zero means this world model may be REPLACED in a later release, not extended. +commands.advancedrocketry.universe.status.stable=Generator %s is a stable release. +commands.advancedrocketry.universe.status.config=Configuration: world %s, pack %s +commands.advancedrocketry.universe.status.agrees=The pack states the same universe this world was generated under. +commands.advancedrocketry.universe.status.differs=The pack states a DIFFERENT universe. Restore the previous , or run /stellurgy universe upgrade to accept it. +commands.advancedrocketry.universe.status.frozen=Frozen by being seen: %s systems +commands.advancedrocketry.universe.status.released=Models this build can still read: %s +commands.advancedrocketry.universe.status.armed=An upgrade is armed: the next start will accept one change to . +commands.advancedrocketry.universe.upgrade.usage=upgrade [confirm] - freeze everything already seen, then accept the pack's current universe +commands.advancedrocketry.universe.upgrade.preview=This would move the world from configuration %s to %s. %s systems are already frozen. +commands.advancedrocketry.universe.upgrade.reach=Memory crystals will be read from the %s player(s) online. Crystals in chests, in unloaded chunks, or carried by players who are offline CANNOT be read - bring them in first. +commands.advancedrocketry.universe.upgrade.confirm=Run /stellurgy universe upgrade confirm to go ahead. This cannot be undone. +commands.advancedrocketry.universe.upgrade.done=Upgrade complete: %s crystals read, %s addresses, %s newly frozen. World model %s -> %s, configuration %s. +commands.advancedrocketry.universe.upgrade.armed=This world will also accept ONE change to at its next start. Stop the server, put the new configuration in place, and start again. +commands.advancedrocketry.universe.upgrade.seam=Charted space keeps exactly what it held; everything beyond it is re-derived under the new model. + commands.advancedrocketry.filldata.usage=/ar fillData OR /ar fillData chip (alias: fd) commands.advancedrocketry.filldata.chip.notheld=Hold an asteroid chip in your main hand to use /ar fillData chip commands.advancedrocketry.filldata.chip.success=Filled asteroid chip with %s data in composition, mass, and distance diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index c5ba5e9b1..52acbf3b6 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -2,7 +2,14 @@ import org.junit.Test; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -11,19 +18,28 @@ import java.util.Set; import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.BodyDerivationV0; +import zmaster587.advancedRocketry.universe.BodyProfile; import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.Cosmology; import zmaster587.advancedRocketry.universe.Galaxy; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.IBodyDerivation; +import zmaster587.advancedRocketry.universe.PlanetDerivation; import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; import zmaster587.advancedRocketry.universe.UniverseScale; +import zmaster587.advancedRocketry.universe.UniverseSchemas; import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * Contract tests for the deterministic clustered galaxy generator. Pure-JUnit; no MC bootstrap. @@ -699,4 +715,377 @@ private static int majorBodies(List bodies) { } return n; } + + // ── the derivation is part of the world model ───────────────────────────── + + /** A derivation that differs from version 1 in one law, and delegates the rest. */ + private static final class ShiftedDerivation implements IBodyDerivation { + private final IBodyDerivation base = BodyDerivationV0.INSTANCE; + + @Override + public double metallicityOf(long seed, GalacticCoord anchor) { + return base.metallicityOf(seed, anchor); + } + + @Override + public int referenceDistance(zmaster587.advancedRocketry.api.dimension.solar.StellarBody star) { + return base.referenceDistance(star); + } + + @Override + public int orbitalDistanceOf(long seed, GalacticCoord anchor, int index, int count, + zmaster587.advancedRocketry.api.dimension.solar.StellarBody star) { + return base.orbitalDistanceOf(seed, anchor, index, count, star) + 7; + } + + @Override + public double innerOrbit(zmaster587.advancedRocketry.api.dimension.solar.StellarBody star) { + return base.innerOrbit(star); + } + + @Override + public double outerOrbit(zmaster587.advancedRocketry.api.dimension.solar.StellarBody star) { + return base.outerOrbit(star); + } + + @Override + public int bareTemperature(zmaster587.advancedRocketry.api.dimension.solar.StellarBody star, + int orbitalDistance) { + return base.bareTemperature(star, orbitalDistance); + } + + @Override + public boolean tidallyLockedAt(zmaster587.advancedRocketry.api.dimension.solar.StellarBody star, + int orbitalDistance) { + return base.tidallyLockedAt(star, orbitalDistance); + } + + @Override + public boolean isGiantAt(long seed, GalacticCoord anchor, int index, int bareTemperatureK) { + return base.isGiantAt(seed, anchor, index, bareTemperatureK); + } + + @Override + public BodyProfile derive(long seed, GalacticCoord anchor, GalacticCoord bodyCell, int variant, + zmaster587.advancedRocketry.api.dimension.solar.StellarBody star, + boolean moon, int orbitalDistance) { + return base.derive(seed, anchor, bodyCell, variant, star, moon, orbitalDistance); + } + + @Override + public BodyProfile deriveRogue(long seed, GalacticCoord bodyCell, int variant, + double giantFraction) { + return base.deriveRogue(seed, bodyCell, variant, giantFraction); + } + + @Override + public int residualTemperature(double massEarths, double radiusEarths) { + return base.residualTemperature(massEarths, radiusEarths); + } + } + + @Test + public void aGeneratorDerivesItsBodiesThroughTheDerivationItWasGiven() { + // The point of the seam: a later schema can change what a body IS while the placement stands. + // If this passes with an unused parameter somewhere, the seam is decoration. + GalaxyGenConfig config = defaultsCfg(); + ClusteredGalaxyGenerator stock = new ClusteredGalaxyGenerator(config); + ClusteredGalaxyGenerator shifted = new ClusteredGalaxyGenerator(config, new ShiftedDerivation()); + + GalacticCoord anchor = null; + for (int i = 0; i < 64 && anchor == null; i++) { + GalacticCoord probe = GalacticCoord.ofSectorLocal((long) i * config.minSpacing, 0, 0, 0, 0, 0); + java.util.Optional found = stock.anchorAt(SEED, probe); + if (found.isPresent() && !stock.bodiesFor(SEED, found.get()).isEmpty()) { + anchor = found.get(); + } + } + assertTrue("arrangement: a system with bodies must be found near the origin", anchor != null); + + List stockBodies = stock.bodiesFor(SEED, anchor); + List shiftedBodies = shifted.bodiesFor(SEED, anchor); + + // The retinue does not merely change VALUES, it changes SHAPE — a body's cell follows its + // orbital distance, so moving the orbit law moves which seats are claimed and how many fit. + // That is the strongest form of the claim being made here: the derivation is not a decoration + // on top of a fixed layout, it is part of what the world model IS, and it therefore has to + // travel with the schema version rather than with the jar. + assertNotEquals("a generator handed a different derivation must produce a different system — " + + "otherwise the derivation is not reachable from the schema at all", + describe(stockBodies), describe(shiftedBodies)); + } + + /** A system as a comparable string: every body's cell, kind and orbit, in a stable order. */ + private static String describe(List bodies) { + List lines = new ArrayList<>(); + for (SystemBody b : bodies) { + lines.add(b.name().cellKey() + ':' + b.kind() + ':' + b.orbitalDistance()); + } + Collections.sort(lines); + return lines.toString(); + } + + @Test + public void aGeneratorHandsOutTheDerivationItUses() { + // How everything outside this package reaches the world's derivation. Asking the class directly + // would pin version 1 forever, whatever schema the save is owed. + IBodyDerivation mine = new ShiftedDerivation(); + + assertSame("a generator must hand out the derivation it was built with", + mine, new ClusteredGalaxyGenerator(defaultsCfg(), mine).derivation()); + assertSame("and the stock one hands out version 1's", BodyDerivationV0.INSTANCE, + new ClusteredGalaxyGenerator(defaultsCfg()).derivation()); + } + + // ── the golden corpus ───────────────────────────────────────────────────── + + /** + * The released world model, rendered and compared byte for byte against a checked-in fixture. + * + *

    This is not a regression test, it is a VERSION DECISION. A save keeps what has been + * touched and re-derives everything else, so any change to what this renders moves systems in worlds + * that already exist. The fixture is what makes that visible before it ships:

    + * + *
      + *
    • No diff — the world model is unchanged; the release is a minor one and existing saves + * carry on under the same schema version.
    • + *
    • A diff, on a version that has REACHED A RELEASE — the world model has moved under + * worlds that exist, so the change needs a NEW schema version registered in + * {@code UniverseSchemas}, and this fixture is regenerated alongside it. Not a discussion: + * a diff here IS the definition of a different universe.
    • + *
    • A diff, on a version that has not shipped yet — the version is edited IN PLACE and + * the fixture regenerated with it. A model nobody outside the branch has ever generated a + * world under owes nobody compatibility, and minting a version for it would fill the registry + * with universes that never existed. "Shipped" means merged to the release branch, not + * landed on a feature branch.
    • + *
    + * + *

    Regenerate deliberately, never to make a red test green: + * {@code ./gradlew testUnit -Dadvancedrocketry.universe.corpus.write=true}

    + */ + @Test + public void theGoldenCorpusIsByteIdentical() throws Exception { + byte[] rendered = UniverseCorpus.render().getBytes(StandardCharsets.UTF_8); + + if (Boolean.getBoolean("advancedrocketry.universe.corpus.write")) { + File out = new File(UniverseCorpus.FIXTURE_PATH); + //noinspection ResultOfMethodCallIgnored + out.getParentFile().mkdirs(); + byte[] tmp = rendered; + try (FileOutputStream fos = new FileOutputStream(out)) { + fos.write(tmp); + } + fail("corpus rewritten to " + out.getPath() + " (" + tmp.length + " bytes). This is a " + + "DELIBERATE act: if the content changed, the world model changed, and the release " + + "needs a new universe schema version. Re-run without the write flag."); + } + + byte[] expected; + try (InputStream in = getClass().getResourceAsStream(UniverseCorpus.FIXTURE_RESOURCE)) { + assertNotNull("the golden corpus fixture is missing from the test resources: " + + UniverseCorpus.FIXTURE_RESOURCE, in); + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int read; + while ((read = in.read(chunk)) > 0) { + buf.write(chunk, 0, read); + } + expected = buf.toByteArray(); + } + + if (!Arrays.equals(expected, rendered)) { + fail("THE WORLD MODEL HAS MOVED. " + firstDifference( + new String(expected, StandardCharsets.UTF_8), + new String(rendered, StandardCharsets.UTF_8)) + + "\nEvery system nobody has visited moves with it, in every save generated under " + + "this version. If the change is NOT intended, this is the bug. If it is: a version " + + "that has already reached a release needs a NEW schema version in UniverseSchemas " + + "beside it, while a version that has not shipped yet is edited in place — it owes " + + "nobody compatibility. Either way the fixture is regenerated deliberately, with " + + "-Dadvancedrocketry.universe.corpus.write=true."); + } + } + + /** The first line that differs, quoted — a byte offset alone says nothing about what moved. */ + private static String firstDifference(String expected, String actual) { + String[] e = expected.split("\n", -1); + String[] a = actual.split("\n", -1); + for (int i = 0; i < Math.max(e.length, a.length); i++) { + String le = i < e.length ? e[i] : ""; + String la = i < a.length ? a[i] : ""; + if (!le.equals(la)) { + return "line " + (i + 1) + ":\n fixture: " + le + "\n now: " + la; + } + } + return "the two differ in length only (" + expected.length() + " vs " + actual.length() + ")"; + } + + /** + * Renders the observable universe of a fixed set of seeds over a fixed region — the whole schema, + * not the generator alone. + * + *

    Four members, and each is sampled where a change to it would show:

    + *
      + *
    • {@code IGalaxyGenerator} — which territories hold a system, its identity, and the cells its + * bodies stand in;
    • + *
    • {@code PlanetDerivation} — a profile derived at each body's real inputs. Deliberately a + * SAMPLE at a fixed variant rather than a claim about what the generator built internally: + * its purpose is to be a canary on the derivation, and a canary that reproduced the + * generator's private choices would be pinning implementation instead;
    • + *
    • {@code UniverseScale} — the metric constants and both conversions, because a light year + * that becomes a different number of cells relocates everything at once;
    • + *
    • {@code Cosmology} — the expansion factor at fixed ticks.
    • + *
    + * + *

    Rendering rules: LF only, every list sorted, doubles through {@link Double#toString} (exact and + * locale-free — a formatted number would hide a change in its last digits and change with a locale).

    + */ + static final class UniverseCorpus { + + static final String FIXTURE_RESOURCE = "/universe/golden-corpus-v1.txt"; + static final String FIXTURE_PATH = "src/test/resources/universe/golden-corpus-v1.txt"; + + /** Fixed seeds. Arbitrary, and that is the point — they are frozen, not chosen for an outcome. */ + private static final long[] SEEDS = { + 1L, 42L, 1337L, 8675309L, -1L, 6_942_069L, 2_147_483_647L, + }; + + /** Territories swept per axis, centred on the origin — the home galaxy's centre. */ + private static final int SPAN = 1; + + private UniverseCorpus() { + } + + static String render() { + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + StringBuilder sb = new StringBuilder(64 * 1024); + sb.append("# universe golden corpus - schema ").append(UniverseSchemas.CURRENT).append('\n'); + sb.append("config ").append(config.fingerprint()).append('\n'); + renderScale(sb); + renderCosmology(sb); + for (long seed : SEEDS) { + renderSeed(sb, config, seed); + } + return sb.toString(); + } + + private static void renderScale(StringBuilder sb) { + sb.append("scale spacingCells=").append(UniverseScale.DEFAULT_SPACING_CELLS) + .append(" galaxySpacingCells=").append(UniverseScale.DEFAULT_GALAXY_SPACING_CELLS) + .append(" seatMarginCells=").append(UniverseScale.SEAT_MARGIN_CELLS).append('\n'); + double[] lightYears = {0.1d, 1d, 4.23d, 100d, 50_000d}; + for (double ly : lightYears) { + long cells = UniverseScale.cellsForLightYears(ly); + sb.append("scale ly=").append(Double.toString(ly)) + .append(" cells=").append(cells) + .append(" backLy=").append(Double.toString(UniverseScale.lightYearsForCells(cells))) + .append('\n'); + } + } + + private static void renderCosmology(StringBuilder sb) { + long[] ticks = {0L, 24_000L, 24_000_000L}; + for (long tick : ticks) { + sb.append("cosmology tick=").append(tick) + .append(" scaleFactor=").append(Double.toString(Cosmology.scaleFactorAt(tick))) + .append('\n'); + } + } + + private static void renderSeed(StringBuilder sb, GalaxyGenConfig config, long seed) { + ClusteredGalaxyGenerator g = new ClusteredGalaxyGenerator(config); + long step = config.minSpacing; + Set seen = new HashSet<>(); + List lines = new ArrayList<>(); + for (int i = -SPAN; i <= SPAN; i++) { + for (int j = -SPAN; j <= SPAN; j++) { + for (int k = -SPAN; k <= SPAN; k++) { + GalacticCoord probe = GalacticCoord.ofSectorLocal(i * step, j * step, k * step, + 0L, 0L, 0L); + Optional anchor = g.anchorAt(seed, probe); + if (!anchor.isPresent() || !seen.add(anchor.get().cellKey())) { + continue; + } + renderSystem(lines, g, seed, anchor.get()); + } + } + } + Collections.sort(lines); + sb.append("seed ").append(seed).append(" systems=").append(seen.size()).append('\n'); + for (String line : lines) { + sb.append(line).append('\n'); + } + } + + private static void renderSystem(List out, ClusteredGalaxyGenerator g, long seed, + GalacticCoord anchor) { + Optional systemOpt = g.systemAt(seed, anchor); + if (!systemOpt.isPresent()) { + return; + } + PlanetarySystem system = systemOpt.get(); + StringBuilder head = new StringBuilder(); + head.append(" system ").append(anchor.cellKey()) + .append(" id=").append(system.systemId()) + .append(" kind=").append(system.primaryKind()) + .append(" name=").append(system.name()); + if (system.star().isPresent()) { + head.append(" starTemp=").append(system.star().get().getTemperature()) + .append(" starSize=").append(Double.toString(system.star().get().getSize())); + } else { + head.append(" starless"); + } + out.add(head.toString()); + + List bodies = new ArrayList<>(g.bodiesFor(seed, anchor)); + List bodyLines = new ArrayList<>(); + // One derivation sample per distinct CELL, not per body: a moon stands in its parent's + // cell, so a per-body sample would render every profile twice and cover nothing extra. + Map byCell = new java.util.TreeMap<>(); + for (SystemBody body : bodies) { + bodyLines.add(renderBody(anchor, body)); + String key = body.name().cellKey(); + if (!byCell.containsKey(key)) { + byCell.put(key, body); + } + } + Collections.sort(bodyLines); + out.addAll(bodyLines); + for (Map.Entry e : byCell.entrySet()) { + out.add(renderDerivation(g, seed, anchor, system, e.getValue())); + } + } + + private static String renderBody(GalacticCoord anchor, SystemBody body) { + return " body " + anchor.cellKey() + ' ' + body.name().cellKey() + + " kind=" + body.kind() + + " orbit=" + body.orbitalDistance() + + " radius=" + Double.toString(body.radiusEarths()) + + " starId=" + body.starId() + + " frame=" + body.definesFrame(); + } + + private static String renderDerivation(ClusteredGalaxyGenerator g, long seed, + GalacticCoord anchor, PlanetarySystem system, + SystemBody body) { + BodyProfile profile = system.star().isPresent() + ? PlanetDerivation.derive(seed, anchor, body.name(), 0, system.star().get(), false, + body.orbitalDistance()) + : PlanetDerivation.deriveRogue(seed, body.name(), 0, + g.config().rogue.giantFraction); + return " derived " + anchor.cellKey() + ' ' + body.name().cellKey() + + " type=" + profile.typeName() + + " mass=" + Double.toString(profile.massEarths()) + + " radius=" + Double.toString(profile.radiusEarths()) + + " gravity=" + profile.gravityPercent() + + " pressure=" + profile.pressure() + + " tempK=" + profile.temperatureKelvin() + + " oxygen=" + profile.hasOxygen() + + " locked=" + profile.tidallyLocked() + + " rings=" + profile.hasRings() + + " rotation=" + profile.rotationalPeriodTicks() + + " metallicity=" + Double.toString(profile.metallicity()) + + " terrain=" + profile.terrain(); + } + } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java index 68fae8cf5..7895dc268 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyFieldTest.java @@ -20,6 +20,7 @@ import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.LightYearVector; import zmaster587.advancedRocketry.universe.PlanetarySystem; +import zmaster587.advancedRocketry.universe.UniverseLawsV0; import zmaster587.advancedRocketry.universe.UniverseScale; import static org.junit.Assert.assertEquals; @@ -44,7 +45,7 @@ private static GalaxyGenConfig cfg(double galaxyDensity) { } private static GalaxyField field(double galaxyDensity) { - return new GalaxyField(cfg(galaxyDensity)); + return new GalaxyField(cfg(galaxyDensity), UniverseLawsV0.INSTANCE); } @Test @@ -121,7 +122,7 @@ public void theGalaxyIndexIsDerivedFromTheSector() { // No stored tier, no new coordinate field: a coarse reading of the sector space that already // exists. Every sector of one galaxy cell must name the same galaxy. GalaxyGenConfig config = cfg(1.0d); - GalaxyField f = new GalaxyField(config); + GalaxyField f = new GalaxyField(config, UniverseLawsV0.INSTANCE); long s = config.galaxySpacing; // The lattice is offset by half a cell, so the ORIGIN is a cell CENTRE. Without that, every // sector with a negative coordinate would sit in a neighbouring cell and the space around the @@ -170,7 +171,7 @@ public void aGalaxyNeverStraddlesItsOwnCellFace() { // Containment is what keeps three things true at once: at most one galaxy per cell, galaxies // that cannot overlap, and an ownership answer that reads the containing cell and nothing else. GalaxyGenConfig config = cfg(1.0d); - GalaxyField f = new GalaxyField(config); + GalaxyField f = new GalaxyField(config, UniverseLawsV0.INSTANCE); long s = config.galaxySpacing; int checked = 0; for (long gx = -3L; gx <= 3L; gx++) { @@ -384,7 +385,7 @@ private static Galaxy referenceSpiral() { return new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, typeNamed(GalaxyGenConfig.defaults(), "Spiral"), UniverseScale.REFERENCE_GALAXY_RADIUS_LY, - 0d, 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO); + 0d, 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO, UniverseLawsV0.INSTANCE); } @Test @@ -421,7 +422,7 @@ public void everySeedsHomeGalaxyIsAPlaceOfTheRightOrder() { // radius cubed means. The band here is therefore wide on purpose; what it guards is that no // seed opens on a village, and that none opens on something the lattice cannot address. GalaxyGenConfig config = GalaxyGenConfig.defaults(); - GalaxyField f = new GalaxyField(config); + GalaxyField f = new GalaxyField(config, UniverseLawsV0.INSTANCE); for (long seed : new long[] {0xC0FFEEL, 1L, 2L, 3L, 17L, 99L}) { Galaxy home = f.home(seed); double systems = estimateSystems(home, config); @@ -711,7 +712,7 @@ public void aGalaxyCannotDriftOutOfItsOwnCell() { // bound is real code, and it is measured here rather than asserted — at realistic speeds it is // orders away from binding, which is the finding. GalaxyGenConfig config = cfg(1.0d); - GalaxyField f = new GalaxyField(config); + GalaxyField f = new GalaxyField(config, UniverseLawsV0.INSTANCE); double halfCellLy = UniverseScale.lightYearsForCells(config.galaxySpacing / 2d); double worstFraction = 0d; int checked = 0; @@ -816,7 +817,7 @@ public void aDeclaredGalaxyIsSeatedWhateverTheHashSays() { assertNotNull("the sweep must find a void galaxy cell to reserve", empty); GalaxyGenConfig reserved = cfg(0.2d).withReservedGalaxies(Collections.singletonList(empty)); - GalaxyField withKey = new GalaxyField(reserved); + GalaxyField withKey = new GalaxyField(reserved, UniverseLawsV0.INSTANCE); assertTrue("a declared key must force its cell to hold a galaxy", withKey.galaxyAtIndex(seed, empty.gx(), empty.gy(), empty.gz()).isPresent()); assertTrue(withKey.isReserved(empty.gx(), empty.gy(), empty.gz())); @@ -830,7 +831,7 @@ public void aGalaxyHoldingAuthoredContentIsDrawnBigEnoughForIt() { long seed = 909L; GalaxyKey key = GalaxyKey.of(6L, -2L, 3L); GalaxyField f = new GalaxyField( - cfg(0.2d).withReservedGalaxies(Collections.singletonList(key))); + cfg(0.2d).withReservedGalaxies(Collections.singletonList(key)), UniverseLawsV0.INSTANCE); Galaxy declared = f.galaxyAtIndex(seed, key.gx(), key.gy(), key.gz()).get(); assertTrue("a reserved galaxy is only " + declared.radiusLy() + " ly across", declared.radiusLy() >= UniverseScale.MIN_AUTHORED_GALAXY_RADIUS_LY); @@ -852,7 +853,7 @@ public void aDeclarationInAnotherGalaxyResolvesAgainstThatGalaxysCentre() { long seed = 77L; GalaxyKey key = GalaxyKey.of(2L, 0L, 0L); GalaxyField f = new GalaxyField( - cfg(1.0d).withReservedGalaxies(Collections.singletonList(key))); + cfg(1.0d).withReservedGalaxies(Collections.singletonList(key)), UniverseLawsV0.INSTANCE); GalacticCoord centre = f.centreOf(seed, key).get(); GalacticCoord local = GalacticCoord.ofSectorLocal(500_000L, 0L, 0L, 0L, 0L, 0L); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java index 8c8c06445..94fa1f091 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/GalaxyTest.java @@ -7,6 +7,7 @@ import zmaster587.advancedRocketry.universe.Galaxy; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.LightYearVector; +import zmaster587.advancedRocketry.universe.UniverseLawsV0; import zmaster587.advancedRocketry.universe.UniverseScale; import static org.junit.Assert.assertEquals; @@ -45,13 +46,13 @@ private static GalaxyGenConfig.GalaxyType dwarf() { /** A galaxy with its plane on the world's XZ plane, so a test can reason in plain coordinates. */ private static Galaxy flat(GalaxyGenConfig.GalaxyType type) { return new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, type, RADIUS, 0d, 0d, - Math.toRadians(20d), 0d, LightYearVector.ZERO); + Math.toRadians(20d), 0d, LightYearVector.ZERO, UniverseLawsV0.INSTANCE); } /** The same galaxy, seated away from the origin and moving — the subject of the R3 laws. */ private static Galaxy adrift(GalacticCoord seat, LightYearVector velocity) { return new Galaxy(1L, 0L, 0L, 0, seat, smoothDisc(), RADIUS, 0d, 0d, Math.toRadians(20d), 0d, - velocity); + velocity, UniverseLawsV0.INSTANCE); } @Test @@ -131,9 +132,9 @@ public void orientationRotatesTheDiscWithoutChangingItsShape() { // Two galaxies alike but for their orientation must be the same object seen from elsewhere: // the density a point sees depends on where it is IN THE GALAXY, never on the world axes. Galaxy flat = new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, 0d, 0d, - Math.toRadians(20d), 0d, LightYearVector.ZERO); + Math.toRadians(20d), 0d, LightYearVector.ZERO, UniverseLawsV0.INSTANCE); Galaxy tilted = new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, smoothDisc(), RADIUS, - Math.toRadians(90d), 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO); + Math.toRadians(90d), 0d, Math.toRadians(20d), 0d, LightYearVector.ZERO, UniverseLawsV0.INSTANCE); // The tilted galaxy's pole is +X, so ITS plane is the world's YZ plane. double r = RADIUS * 0.3d; assertEquals("the same point of the galaxy must read the same however it is oriented", @@ -256,7 +257,7 @@ public void theCentreLawIsEvaluatedNeverIntegrated() { LightYearVector.of(3e-10d, -1e-10d, 2e-10d)); long t = 12_345_678L; double a = Cosmology.scaleFactorAt(t); - LightYearVector expected = LightYearVector.ofCell(g.centre()) + LightYearVector expected = LightYearVector.ofCell(g.centre(), UniverseLawsV0.INSTANCE) .plus(g.peculiarVelocity().scale((double) t)).scale(a); assertEquals(expected.x(), g.centreAt(t).x(), Math.abs(expected.x()) * 1e-12d); assertEquals(expected.y(), g.centreAt(t).y(), 1e-9d); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java index d3f0d0130..946448666 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SkyNebulaeProducerTest.java @@ -15,6 +15,7 @@ import zmaster587.advancedRocketry.universe.IGalaxyGenerator; import zmaster587.advancedRocketry.universe.Nebula; import zmaster587.advancedRocketry.universe.PlanetarySystem; +import zmaster587.advancedRocketry.universe.UniverseLawsV0; import zmaster587.advancedRocketry.universe.UniverseScale; import static org.junit.Assert.assertEquals; @@ -35,7 +36,7 @@ public class SkyNebulaeProducerTest { /** A cloud seated at a stated point, with a stated size. The cluster behind it is not read here. */ private static Nebula cloudAt(double xLy, double yLy, double zLy, double radiusLy) { - return new Nebula(null, Nebula.Appearance.EMISSION, xLy, yLy, zLy, radiusLy, 0.8d); + return new Nebula(null, Nebula.Appearance.EMISSION, xLy, yLy, zLy, radiusLy, 0.8d, UniverseLawsV0.INSTANCE); } /** A generator that answers with exactly these clouds, whatever is asked. */ @@ -158,7 +159,7 @@ public void whatIsSeatedAndWhatIsDrawnAreSeparatelyReadable() { public void aCloudCarriesItsAppearanceAndItsThickness() { // The two fields the renderer branches on: the age sequence decides the tint, and a dark // cloud is the one that must be drawn OVER the stars rather than behind them. - Nebula dark = new Nebula(null, Nebula.Appearance.DARK, 0d, 0d, 150d, 40d, 0.6d); + Nebula dark = new Nebula(null, Nebula.Appearance.DARK, 0d, 0d, 150d, 40d, 0.6d, UniverseLawsV0.INSTANCE); RenderNebula drawn = SkyNebulaeProducer.renderOf(dark, 0d, 0d, 0d); assertNotNull(drawn); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java index 9b1d985b3..3760c9482 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java @@ -10,6 +10,7 @@ import zmaster587.advancedRocketry.universe.Galaxy; import zmaster587.advancedRocketry.universe.GalaxyGenConfig; import zmaster587.advancedRocketry.universe.StarCluster; +import zmaster587.advancedRocketry.universe.UniverseLawsV0; import zmaster587.advancedRocketry.universe.UniverseScale; import static org.junit.Assert.assertEquals; @@ -158,7 +159,7 @@ public void aNUCLEUSscalesToItsOwnGalaxyWhileTheOtherClustersDoNot() { private static Galaxy galaxyOfRadius(double radiusLy) { return new Galaxy(0L, 0L, 0L, 0, GalacticCoord.ORIGIN, cfg().galaxyTypes.get(0), radiusLy, 0d, 0d, Math.toRadians(20d), 0d, - zmaster587.advancedRocketry.universe.LightYearVector.ZERO); + zmaster587.advancedRocketry.universe.LightYearVector.ZERO, UniverseLawsV0.INSTANCE); } @Test diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java index a0d84acd6..40e96a79c 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -449,6 +450,115 @@ public void aSurveyResolvesPerLookAndNeverWalksTheGalaxy() { counting.queries <= budget); } + // ── a look is a touch ───────────────────────────────────────────────────── + + /** How a system reads to a test: what it is, and where each of its bodies stands. */ + private static String describe(UniverseRegistry registry, GalacticCoord anchor) { + StringBuilder sb = new StringBuilder(); + // Asked through systemForCoord, which answers pinned OR derived. starIdForCoord reads the + // override store alone, so it would report the PIN rather than the system and turn "this system + // did not move" into "this system is now in the store", which is a different claim. + sb.append(registry.systemForCoord(anchor) + .map(s -> s.systemId() + "/" + s.primaryKind() + "/" + s.name()) + .orElse("none")); + java.util.List bodies = new java.util.ArrayList<>(); + for (SystemBody b : registry.systemBodiesAt(anchor)) { + bodies.add(b.name().cellKey() + ':' + b.kind() + ':' + b.radiusEarths()); + } + java.util.Collections.sort(bodies); + return sb.append(bodies).toString(); + } + + /** The same universe with one knob moved — everything untouched is derived differently under it. */ + private static GalaxyGenConfig retuned() { + return new GalaxyGenConfig(GalaxyGenConfig.DEFAULT_MIN_SPACING, 0.9d, + GalaxyGenConfig.DEFAULT_GALAXY_SPACING, GalaxyGenConfig.DEFAULT_GALAXY_DENSITY, + null, null); + } + + @Test + public void aSystemAScanReportedIsFrozenAgainstALaterRetune() { + // The promise the whole schema-versioning rests on: what the player has SEEN stops moving. + // A survey answers out of the derivation, so without a pin the system on his crystal is a + // function of the pack's current knobs — and he finds that out by flying there. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(config)); + UniverseRegistry.setStarLookup(TelescopeRegionScanTest::star); + UniverseRegistry registry = new UniverseRegistry(); + registry.bindWorldSeed(0xC0FFEEL); + + GalacticCoord looked = cell(0, 0, 0); + GalacticCoord neverLooked = cell(3 * STEP, 0, 0); + GalacticCoord lookedAnchor = registry.anchorForCell(looked).orElse(null); + GalacticCoord otherAnchor = registry.anchorForCell(neverLooked).orElse(null); + assertNotNull("arrangement: the looked-at cell must hold a system", lookedAnchor); + assertNotNull("arrangement: the control cell must hold a system", otherAnchor); + String lookedBefore = describe(registry, lookedAnchor); + String otherBefore = describe(registry, otherAnchor); + + CrystalMemory crystal = new CrystalMemory(); + assertTrue("arrangement: the look must report something", + TelescopeScan.resolveCell(registry, looked, crystal, 7_000L, dimId -> "Body-" + dimId) > 0); + + UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(retuned())); + + assertNotEquals("arrangement: the retune must actually move an untouched system, or this test " + + "proves nothing", otherBefore, describe(registry, otherAnchor)); + assertEquals("a system a telescope reported must survive a retune of the universe it was " + + "derived from", lookedBefore, describe(registry, lookedAnchor)); + } + + @Test + public void aLookIntoTheVoidFreezesNothing() { + // The pin must follow the REPORT, not the look: freezing empty sky would fill the save with + // snapshots of nothing and take space out of the pack author's hands for no promise made. + UniverseRegistry registry = threeSystems(); + CrystalMemory crystal = new CrystalMemory(); + + TelescopeScan.resolveCell(registry, cell(400 * STEP, 0, 0), crystal, 7_000L, + dimId -> "Body-" + dimId); + + assertEquals("a look at nothing must write no snapshot into the save", 0, pinnedCount(registry)); + } + + @Test + public void whatASurveyFreezesIsMeasuredNotAssumed() { + // A pin snapshots a whole system, so a wide sweep is a write. The cost is stated here as a + // NUMBER rather than asserted to be small: the bound below is a tripwire against an order of + // magnitude, and the printed figures are what a decision about survey width is made from. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(config)); + UniverseRegistry.setStarLookup(TelescopeRegionScanTest::star); + UniverseRegistry registry = new UniverseRegistry(); + registry.bindWorldSeed(0xC0FFEEL); + + RegionScan.Tuning live = new RegionScan.Tuning(100d, 1, 512, 100, 50d, 4, config.minSpacing); + RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, live.maxRangeSteps(), 0L, live); + int looks = scan.totalCells(); + CrystalMemory crystal = new CrystalMemory(); + + long startedAt = System.nanoTime(); + TelescopeScan.resolveBatch(registry, scan, 0, looks, crystal, 7_000L, dimId -> "Body-" + dimId); + long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000L; + + NBTTagCompound tag = new NBTTagCompound(); + registry.writeToNBT(tag); + int pins = pinnedCount(registry); + int bytes = tag.toString().length(); + System.out.println("survey of " + looks + " looks froze " + pins + " systems in " + elapsedMs + + " ms; the universe save renders as " + bytes + " chars"); + + assertTrue("a survey must not freeze more systems than it had looks (" + pins + " pins for " + + looks + " looks)", pins <= looks); + assertTrue("arrangement: the sweep must have frozen something", pins > 0); + } + + private static int pinnedCount(UniverseRegistry registry) { + NBTTagCompound tag = new NBTTagCompound(); + registry.writeToNBT(tag); + return tag.getTagList("pinnedSystems", 10).tagCount(); + } + @Test public void aLookIntoTheVoidDiscoversNothing() { // The gate exists so that empty sky does not manufacture addresses — and the fix must not diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java index 8597fd558..55993ffb9 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/UniverseRegistryTest.java @@ -23,13 +23,20 @@ import zmaster587.advancedRocketry.universe.PlanetarySystem; import zmaster587.advancedRocketry.universe.SystemBody; import zmaster587.advancedRocketry.universe.SystemBodyKind; +import zmaster587.advancedRocketry.universe.IUniverseLaws; +import zmaster587.advancedRocketry.universe.UniverseLawsV0; import zmaster587.advancedRocketry.universe.UniverseRegistry; +import zmaster587.advancedRocketry.universe.UniverseSchema; +import zmaster587.advancedRocketry.universe.UniverseSchemaMismatchException; +import zmaster587.advancedRocketry.universe.UniverseSchemas; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * Contract tests for the Layer-1 universe registry: the cell-keyed coord↔system placement @@ -752,6 +759,406 @@ public void interstellarVoidIsFedNothing() { assertTrue("the space between stars is black", reg.skyBodiesAt(farAway).isEmpty()); } + // ── the world-model stamp ───────────────────────────────────────────────── + + /** The configuration a pack states, and a retuned one — one knob apart. */ + private static GalaxyGenConfig packConfig() { + return GalaxyGenConfig.defaults(); + } + + private static GalaxyGenConfig retunedConfig() { + return GalaxyGenConfig.defaults().withRogueTuning( + new GalaxyGenConfig.RogueTuning(7d, 0.012d, 3d, GalaxyGenConfig.defaultRogueTypes())); + } + + @Test + public void aFreshWorldTakesTheCurrentModelAndRecordsIt() { + // Nothing to reconcile against: a new world is generated under whatever this build ships, and + // that fact is written down so the NEXT load has something to check. + UniverseRegistry reg = new UniverseRegistry(); + assertEquals("a world with no history carries no stamp", UniverseRegistry.UNSTAMPED, + reg.schemaVersion()); + + UniverseSchema schema = reg.reconcileSchema(packConfig()); + + assertEquals("a fresh world is generated under the current model", + UniverseSchemas.CURRENT, schema.version()); + assertEquals("and the model it was generated under is recorded", + UniverseSchemas.CURRENT, reg.schemaVersion()); + assertEquals("along with the configuration that produced it", + packConfig().fingerprint(), reg.configFingerprint()); + } + + @Test + public void theModelAWorldWasGeneratedUnderSurvivesASave() { + UniverseRegistry source = new UniverseRegistry(); + source.reconcileSchema(packConfig()); + + NBTTagCompound tag = new NBTTagCompound(); + source.writeToNBT(tag); + UniverseRegistry round = new UniverseRegistry(); + round.readFromNBT(tag); + + assertEquals("the schema version must outlive the session", source.schemaVersion(), + round.schemaVersion()); + assertEquals("and so must the configuration it was generated under", + source.configFingerprint(), round.configFingerprint()); + } + + @Test + public void theSameConfigurationOpensTheWorldUnchanged() { + // The ordinary case, and the one that must never cost the player anything: same pack, same + // build, second boot. + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(packConfig()); + + NBTTagCompound tag = new NBTTagCompound(); + reg.writeToNBT(tag); + UniverseRegistry reopened = new UniverseRegistry(); + reopened.readFromNBT(tag); + + UniverseSchema schema = reopened.reconcileSchema(packConfig()); + assertEquals("an unchanged world opens under the model it was made with", + UniverseSchemas.CURRENT, schema.version()); + } + + @Test + public void aRetunedConfigurationIsRefusedRatherThanSubstituted() { + // The defect this whole stamp exists for: a pack edit silently re-deriving every system a + // player has not visited. It must stop the load, not warn into a log nobody reads. + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(packConfig()); + NBTTagCompound tag = new NBTTagCompound(); + reg.writeToNBT(tag); + UniverseRegistry reopened = new UniverseRegistry(); + reopened.readFromNBT(tag); + + try { + reopened.reconcileSchema(retunedConfig()); + fail("a world whose has been retuned must not load silently"); + } catch (UniverseSchemaMismatchException expected) { + assertTrue("the refusal must name the configuration the world was made under: " + + expected.getMessage(), + expected.getMessage().contains(packConfig().fingerprint())); + assertTrue("and the one the pack now states: " + expected.getMessage(), + expected.getMessage().contains(retunedConfig().fingerprint())); + } + } + + @Test + public void aWorldFromAModelThisBuildDoesNotCarryIsRefused() { + // A save from a newer jar. There is no honest way to open it: this build cannot reproduce the + // universe it describes, and deriving a different one under the same save is the silent + // corruption the refusal exists to prevent. + UniverseRegistry reg = new UniverseRegistry(); + NBTTagCompound tag = new NBTTagCompound(); + reg.writeToNBT(tag); + tag.setInteger("schemaVersion", 9999); + tag.setString("galaxyConfigFingerprint", packConfig().fingerprint()); + UniverseRegistry fromTheFuture = new UniverseRegistry(); + fromTheFuture.readFromNBT(tag); + + try { + fromTheFuture.reconcileSchema(packConfig()); + fail("a world from an unknown schema version must not load"); + } catch (UniverseSchemaMismatchException expected) { + assertTrue("the refusal must name the version the world needs: " + expected.getMessage(), + expected.getMessage().contains("9999")); + } + } + + @Test + public void anUpgradeAcceptsTheNewConfigurationDeliberately() { + // The door out of the refusal above: the player asks for it, and afterwards the world opens + // under what the pack now says. + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(packConfig()); + + reg.adoptSchema(retunedConfig()); + + assertEquals("an upgrade records the configuration it accepted", + retunedConfig().fingerprint(), reg.configFingerprint()); + assertEquals("under the current model", UniverseSchemas.CURRENT, reg.schemaVersion()); + assertEquals("and the world then opens without complaint", UniverseSchemas.CURRENT, + reg.reconcileSchema(retunedConfig()).version()); + } + + @Test + public void theLawsAWorldWasGeneratedUnderAreRecordedTheSameWay() { + // The metric and the expansion are stamped, not versioned by implementation: a changed metric + // means every address denotes a different distance, which no existing world can be RUN under. + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(packConfig()); + + assertEquals("a fresh world records the laws it was generated under", + UniverseRegistry.currentLawsFingerprint(), reg.lawsFingerprint()); + + NBTTagCompound tag = new NBTTagCompound(); + reg.writeToNBT(tag); + UniverseRegistry round = new UniverseRegistry(); + round.readFromNBT(tag); + assertEquals("and they outlive the session", reg.lawsFingerprint(), round.lawsFingerprint()); + } + + @Test + public void theShippedModelIsTheAlphaAndSaysSo() { + // The leading zero is the whole statement: this model may be REPLACED rather than extended, and + // a player is told so on any world that uses it. + UniverseSchema current = UniverseSchemas.current(); + + assertEquals("the first released model is version 0", 0, current.version()); + assertEquals("and its human label carries the zero", "0.1", current.label()); + assertFalse("a 0.x label is not a stable release", current.isStable()); + } + + @Test + public void anAbsentStampIsNotReadAsVersionZero() { + // The trap that version 0 creates: NBT answers 0 for an absent integer, and 0 is now a real + // version. Reading the value instead of asking whether the key exists would report every + // stampless save as "generated by the alpha" and skip the adoption a fresh world is owed. + NBTTagCompound bare = new NBTTagCompound(); + assertEquals("arrangement: NBT must indeed default an absent integer to zero", + 0, bare.getInteger("schemaVersion")); + + UniverseRegistry reg = new UniverseRegistry(); + reg.readFromNBT(bare); + + assertEquals("a save with no stamp must read as UNSTAMPED, not as the alpha", + UniverseRegistry.UNSTAMPED, reg.schemaVersion()); + assertTrue("and UNSTAMPED must be a value no version can take", + UniverseRegistry.UNSTAMPED < 0); + } + + @Test + public void anAlphaWorldIsRecognisedAsStampedAfterAReload() { + // The other half of the same trap: a world genuinely generated under version 0 must come back + // as version 0, not as "never stamped". + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(packConfig()); + assertEquals("arrangement: the fresh world takes the alpha", 0, reg.schemaVersion()); + + NBTTagCompound tag = new NBTTagCompound(); + reg.writeToNBT(tag); + UniverseRegistry reopened = new UniverseRegistry(); + reopened.readFromNBT(tag); + + assertEquals("an alpha world must reload as the alpha", 0, reopened.schemaVersion()); + assertEquals("and its configuration must not have been re-adopted", + reg.configFingerprint(), reopened.configFingerprint()); + } + + @Test + public void aVersionsLawsTravelWithIt() { + // The point of the whole exercise: selecting a version selects the metric too, so a build that + // ships a new one does not re-measure the worlds already made under the old. + UniverseSchema v1 = UniverseSchemas.current(); + + assertSame("the generator a schema builds must measure by that schema's laws", + v1.laws(), v1.generator(packConfig()).laws()); + assertEquals("and the stamp is that schema's laws, measured", + UniverseRegistry.lawsFingerprintOf(v1.laws()), + UniverseRegistry.currentLawsFingerprint()); + } + + @Test + public void theLawsFingerprintMeasuresBehaviourNotDeclarations() { + // Taken by RUNNING the conversions, so an implementation whose internal constant moved is caught + // even though it publishes the same list of names. + IUniverseLaws shifted = new ShiftedLaws(); + + assertNotEquals("one cell of difference in one conversion must change the identity", + UniverseRegistry.lawsFingerprintOf(UniverseLawsV0.INSTANCE), + UniverseRegistry.lawsFingerprintOf(shifted)); + } + + /** Version 1's laws with a single conversion moved — a stand-in for a version that measures anew. */ + private static final class ShiftedLaws implements IUniverseLaws { + private final IUniverseLaws base = UniverseLawsV0.INSTANCE; + + @Override + public long cellsForLightYears(double lightYears) { + return base.cellsForLightYears(lightYears) + 1L; + } + + @Override + public long cellsAt(double lightYears) { + return base.cellsAt(lightYears); + } + + @Override + public double lightYearsForCells(double cells) { + return base.lightYearsForCells(cells); + } + + @Override + public double lightYearsPerTick(double kilometresPerSecond) { + return base.lightYearsPerTick(kilometresPerSecond); + } + + @Override + public long cellsForOrbitUnits(double orbitUnits) { + return base.cellsForOrbitUnits(orbitUnits); + } + + @Override + public double orbitUnitsForCells(long cells) { + return base.orbitUnitsForCells(cells); + } + + @Override + public long seatMarginCells(long spacingCells) { + return base.seatMarginCells(spacingCells); + } + + @Override + public double retinueReachLy(double primaryRadiusLy) { + return base.retinueReachLy(primaryRadiusLy); + } + + @Override + public double scaleFactorAt(long tick) { + return base.scaleFactorAt(tick); + } + + @Override + public long driftHorizonTicks() { + return base.driftHorizonTicks(); + } + } + + @Test + public void aReleasedVersionWhoseLawsWereEditedInPlaceIsRefused() { + // A released version's laws may never move: a changed metric ships as a NEW version, which old + // worlds simply do not use. So a mismatch here is not a player's situation at all — it says this + // jar's schema 1 is not the schema 1 that made the world, and nobody downstream can accept that + // away. + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(packConfig()); + NBTTagCompound tag = new NBTTagCompound(); + reg.writeToNBT(tag); + tag.setString("universeLawsFingerprint", "0000deadbeef0000"); + UniverseRegistry otherLaws = new UniverseRegistry(); + otherLaws.readFromNBT(tag); + + try { + otherLaws.reconcileSchema(packConfig()); + fail("a world generated under different laws must not load silently"); + } catch (UniverseSchemaMismatchException expected) { + assertTrue("the refusal must name the laws the world was made under: " + + expected.getMessage(), expected.getMessage().contains("0000deadbeef0000")); + assertTrue("and what this build's schema 1 measures: " + expected.getMessage(), + expected.getMessage().contains(UniverseRegistry.currentLawsFingerprint())); + assertFalse("it must not blame the pack's configuration, which has not moved: " + + expected.getMessage(), expected.getMessage().contains(" configuration")); + } + } + + @Test + public void anUpgradeMayNotAcceptEditedLaws() { + // The one door that must NOT open. A configuration is the pack author's to change and an + // operator may accept it; a released version's laws moving is a broken build, and accepting it + // would silently re-measure everything the world already holds. + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(packConfig()); + reg.armUpgrade(); + NBTTagCompound tag = new NBTTagCompound(); + reg.writeToNBT(tag); + tag.setString("universeLawsFingerprint", "0000deadbeef0000"); + UniverseRegistry brokenBuild = new UniverseRegistry(); + brokenBuild.readFromNBT(tag); + + try { + brokenBuild.reconcileSchema(packConfig()); + fail("an armed upgrade must not accept a released version's laws having moved"); + } catch (UniverseSchemaMismatchException expected) { + assertTrue("the permission must still be standing, unspent", brokenBuild.isUpgradeArmed()); + } + } + + @Test + public void anArmedUpgradeIsSpentOnceAndOnlyOnce() { + // The remedy has to outlive the session that authorised it: a changed stops the + // load, so the permission is given while the world still opens and spent at the boot after. + // Once — a second edit is a second decision. + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(packConfig()); + reg.armUpgrade(); + + NBTTagCompound tag = new NBTTagCompound(); + reg.writeToNBT(tag); + UniverseRegistry nextBoot = new UniverseRegistry(); + nextBoot.readFromNBT(tag); + assertTrue("the permission must survive the restart it exists to cross", + nextBoot.isUpgradeArmed()); + + nextBoot.reconcileSchema(retunedConfig()); + assertEquals("the armed load accepts the new configuration", + retunedConfig().fingerprint(), nextBoot.configFingerprint()); + assertFalse("and the permission is spent", nextBoot.isUpgradeArmed()); + + GalaxyGenConfig retunedAgain = GalaxyGenConfig.defaults().withRogueTuning( + new GalaxyGenConfig.RogueTuning(3d, 0.012d, 3d, GalaxyGenConfig.defaultRogueTypes())); + try { + nextBoot.reconcileSchema(retunedAgain); + fail("a second configuration change must be refused like any other"); + } catch (UniverseSchemaMismatchException expected) { + assertTrue("and the refusal must still say how to accept it deliberately: " + + expected.getMessage(), expected.getMessage().contains("upgrade confirm")); + } + } + + @Test + public void anArmedWorldWhoseConfigurationDidNotChangeKeepsItsPermission() { + // Arming is not a countdown: a world that boots unchanged has spent nothing, and the operator + // who armed it can still make the edit he armed it for. + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(packConfig()); + reg.armUpgrade(); + + reg.reconcileSchema(packConfig()); + + assertTrue("an unchanged load must not consume the permission", reg.isUpgradeArmed()); + } + + @Test + public void anAuthoredOnlyUniverseHasAModelOfItsOwn() { + // No is a legitimate world, not a missing configuration — and it is a DIFFERENT + // world from one that declares a generator, so the two must not share a fingerprint. + UniverseRegistry reg = new UniverseRegistry(); + reg.reconcileSchema(null); + + assertEquals("an authored-anchors-only world is stamped like any other", + UniverseSchemas.CURRENT, reg.schemaVersion()); + assertNotEquals("declaring no generator is not the same universe as declaring one", + packConfig().fingerprint(), reg.configFingerprint()); + assertEquals("and it reopens unchanged", UniverseSchemas.CURRENT, + reg.reconcileSchema(null).version()); + } + + @Test + public void aConfigurationsFingerprintIsAboutTheUniverseItDescribes() { + // Two configurations that describe the same universe must agree, or every load is a false + // alarm; two that describe different ones must differ, or the check sees nothing. + assertEquals("the same knobs must fingerprint the same, run after run", + GalaxyGenConfig.defaults().fingerprint(), GalaxyGenConfig.defaults().fingerprint()); + assertNotEquals("a retuned knob is a different universe", + GalaxyGenConfig.defaults().fingerprint(), retunedConfig().fingerprint()); + } + + @Test + public void reservingAGalaxyKeepsWhatThePackSaidAboutRogues() { + // Authored anchors are folded in AFTER is read, so the fold must not quietly drop + // the rest of what the pack stated — the stamp would then record a universe nobody authored. + GalaxyGenConfig authored = retunedConfig(); + GalaxyGenConfig withGalaxy = authored.withReservedGalaxies( + java.util.Collections.singletonList(zmaster587.advancedRocketry.universe.GalaxyKey.of(1L, 0L, 0L))); + + assertEquals("the authored rogue abundance must survive reserving a galaxy", + authored.rogue.abundance, withGalaxy.rogue.abundance, 0d); + assertEquals("and so must the rest of the tuning", authored.rogue.giantFraction, + withGalaxy.rogue.giantFraction, 0d); + } + /** A test generator that claims every cell with one fixed system — to prove stored placements win. */ private static final class AllClaimingGenerator implements IGalaxyGenerator { private final StellarBody body; diff --git a/src/test/resources/universe/golden-corpus-v1.txt b/src/test/resources/universe/golden-corpus-v1.txt new file mode 100644 index 000000000..a72819f9e --- /dev/null +++ b/src/test/resources/universe/golden-corpus-v1.txt @@ -0,0 +1,1511 @@ +# universe golden corpus - schema 0 +config d838c54e8bdec274 +scale spacingCells=5002361 galaxySpacingCells=2956478272682 seatMarginCells=93499 +scale ly=0.1 cells=118260 backLy=0.10000073490540135 +scale ly=1.0 cells=1182592 backLy=1.0000005842486757 +scale ly=4.23 cells=5002362 backLy=4.230000644874457 +scale ly=100.0 cells=118259131 backLy=100.00000007842154 +scale ly=50000.0 cells=59129565454 backLy=50000.000000313135 +cosmology tick=0 scaleFactor=1.0 +cosmology tick=24000 scaleFactor=1.0000000000014915 +cosmology tick=24000000 scaleFactor=1.0000000014914552 +seed 1 systems=27 + body -146732_3144538_9058898 -146732_3144538_9058898 kind=MOON orbit=0 radius=0.9877675872558382 starId=-478929905 frame=false + body -146732_3144538_9058898 -146732_3144538_9058898 kind=ROGUE_PLANET orbit=0 radius=0.365137001300595 starId=-478929905 frame=true + body -1990844_4373138_-3056240 -1990792_4373137_-3056198 kind=MOON orbit=355 radius=0.28953652111498945 starId=-1524375109 frame=false + body -1990844_4373138_-3056240 -1990792_4373137_-3056198 kind=PLANET orbit=355 radius=1.2959853165262225 starId=-1524375109 frame=true + body -1990844_4373138_-3056240 -1990836_4373138_-3056240 kind=MOON orbit=45 radius=0.4447060804848286 starId=-1524375109 frame=false + body -1990844_4373138_-3056240 -1990836_4373138_-3056240 kind=PLANET orbit=45 radius=2.0020815425545506 starId=-1524375109 frame=true + body -1990844_4373138_-3056240 -1990844_4373138_-3056240 kind=STAR orbit=0 radius=0.0 starId=-1524375109 frame=true + body -1990844_4373138_-3056240 -1990845_4373138_-3056241 kind=MOON orbit=10 radius=0.22470924311305454 starId=-1524375109 frame=false + body -1990844_4373138_-3056240 -1990845_4373138_-3056241 kind=MOON orbit=10 radius=0.6117166796218898 starId=-1524375109 frame=false + body -1990844_4373138_-3056240 -1990845_4373138_-3056241 kind=PLANET orbit=10 radius=0.3898083310932754 starId=-1524375109 frame=true + body -1990844_4373138_-3056240 -1990851_4373138_-3056227 kind=ASTEROID_BELT orbit=80 radius=0.0 starId=-1524375109 frame=true + body -1990844_4373138_-3056240 -1990857_4373137_-3056263 kind=GAS_GIANT orbit=144 radius=6.336033623503909 starId=-1524375109 frame=true + body -1990844_4373138_-3056240 -1990868_4373135_-3056136 kind=ASTEROID_BELT orbit=568 radius=0.0 starId=-1524375109 frame=true + body -2174817_-2291967_261255 -2174817_-2291967_261255 kind=ROGUE_PLANET orbit=0 radius=1.3479949218897396 starId=-1472462165 frame=true + body -3680127_6497425_-2974555 -3680127_6497425_-2974555 kind=MOON orbit=0 radius=2.3820978624799545 starId=-1983124585 frame=false + body -3680127_6497425_-2974555 -3680127_6497425_-2974555 kind=ROGUE_PLANET orbit=0 radius=1.7790054869434075 starId=-1983124585 frame=true + body -3867498_449288_4118676 -3867498_449288_4118676 kind=ROGUE_PLANET orbit=0 radius=0.2746107364620147 starId=-1943963613 frame=true + body -4373024_7511005_1816766 -4370934_7510969_1815501 kind=ASTEROID_BELT orbit=13067 radius=0.0 starId=-110594437 frame=true + body -4373024_7511005_1816766 -4372159_7511003_1818025 kind=PLANET orbit=8167 radius=1.3472896720527072 starId=-110594437 frame=true + body -4373024_7511005_1816766 -4372915_7511005_1817033 kind=STAR orbit=1543 radius=104.0277603185177 starId=-110594438 frame=true + body -4373024_7511005_1816766 -4372992_7511006_1816751 kind=ASTEROID_BELT orbit=189 radius=0.0 starId=-110594437 frame=true + body -4373024_7511005_1816766 -4373006_7511005_1816753 kind=PLANET orbit=119 radius=0.6040809505464881 starId=-110594437 frame=true + body -4373024_7511005_1816766 -4373024_7511005_1816766 kind=STAR orbit=0 radius=0.0 starId=-110594437 frame=true + body -4373024_7511005_1816766 -4373066_7511005_1816718 kind=GAS_GIANT orbit=341 radius=3.461531189557112 starId=-110594437 frame=true + body -4373024_7511005_1816766 -4373066_7511005_1816718 kind=MOON orbit=341 radius=0.20073377523039576 starId=-110594437 frame=false + body -4373024_7511005_1816766 -4373066_7511005_1816718 kind=MOON orbit=341 radius=0.24686627491590246 starId=-110594437 frame=false + body -4752099_9055058_8335985 -4752099_9055058_8335985 kind=MOON orbit=0 radius=1.1667301892464876 starId=-392839469 frame=false + body -4752099_9055058_8335985 -4752099_9055058_8335985 kind=ROGUE_PLANET orbit=0 radius=0.24557082150399945 starId=-392839469 frame=true + body -4890357_-2814414_6240914 -4890357_-2814414_6240914 kind=ROGUE_PLANET orbit=0 radius=1.6732860210523022 starId=-759681393 frame=true + body -670991_-4121660_-2164613 -670621_-4121674_-2164509 kind=ASTEROID_BELT orbit=2054 radius=0.0 starId=-262321917 frame=true + body -670991_-4121660_-2164613 -670902_-4121657_-2164531 kind=GAS_GIANT orbit=646 radius=7.1517935902824386 starId=-262321917 frame=true + body -670991_-4121660_-2164613 -670986_-4121661_-2164629 kind=ASTEROID_BELT orbit=90 radius=0.0 starId=-262321917 frame=true + body -670991_-4121660_-2164613 -670989_-4121660_-2164613 kind=PLANET orbit=11 radius=2.341480090625241 starId=-262321917 frame=true + body -670991_-4121660_-2164613 -670991_-4121660_-2164613 kind=STAR orbit=0 radius=0.0 starId=-262321917 frame=true + body -670991_-4121660_-2164613 -670996_-4121660_-2164616 kind=MOON orbit=31 radius=0.4448751111389811 starId=-262321917 frame=false + body -670991_-4121660_-2164613 -670996_-4121660_-2164616 kind=MOON orbit=31 radius=0.4933305978587888 starId=-262321917 frame=false + body -670991_-4121660_-2164613 -670996_-4121660_-2164616 kind=PLANET orbit=31 radius=0.27867397404199995 starId=-262321917 frame=true + body -670991_-4121660_-2164613 -671002_-4121659_-2164570 kind=PLANET orbit=238 radius=1.8533917606053845 starId=-262321917 frame=true + body -670991_-4121660_-2164613 -671003_-4121660_-2164619 kind=MOON orbit=71 radius=0.20106576597243764 starId=-262321917 frame=false + body -670991_-4121660_-2164613 -671003_-4121660_-2164619 kind=MOON orbit=71 radius=0.5236136532220199 starId=-262321917 frame=false + body -670991_-4121660_-2164613 -671003_-4121660_-2164619 kind=PLANET orbit=71 radius=0.5699188917970125 starId=-262321917 frame=true + body -670991_-4121660_-2164613 -671008_-4121660_-2164588 kind=GAS_GIANT orbit=162 radius=4.148350959171349 starId=-262321917 frame=true + body -670991_-4121660_-2164613 -671008_-4121660_-2164588 kind=MOON orbit=162 radius=0.28685003312625307 starId=-262321917 frame=false + body -670991_-4121660_-2164613 -671165_-4121668_-2164448 kind=PLANET orbit=1284 radius=1.518213839049826 starId=-262321917 frame=true + body 1194346_-4530025_6629562 1191593_-4530197_6633767 kind=MOON orbit=26895 radius=0.2780300806379663 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1191593_-4530197_6633767 kind=MOON orbit=26895 radius=0.3160862042273091 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1191593_-4530197_6633767 kind=PLANET orbit=26895 radius=0.3710815068256704 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1194117_-4530031_6629817 kind=GAS_GIANT orbit=1835 radius=3.260041612564983 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1194117_-4530031_6629817 kind=MOON orbit=1835 radius=0.2002745486322834 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1194162_-4530030_6629610 kind=ASTEROID_BELT orbit=1019 radius=0.0 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1194189_-4530017_6629650 kind=MOON orbit=961 radius=0.4493509992896797 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1194189_-4530017_6629650 kind=PLANET orbit=961 radius=1.910283521314024 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1194309_-4530025_6629559 kind=PLANET orbit=197 radius=0.5175670411566737 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1194313_-4530027_6629512 kind=MOON orbit=323 radius=0.2644569179326812 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1194313_-4530027_6629512 kind=MOON orbit=323 radius=0.28093495512902344 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1194313_-4530027_6629512 kind=PLANET orbit=323 radius=1.986963906941915 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1194346_-4530025_6629562 kind=STAR orbit=0 radius=0.0 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1194351_-4530025_6629565 kind=STAR orbit=31 radius=89.65360039293766 starId=-962147134 frame=true + body 1194346_-4530025_6629562 1194475_-4530022_6629580 kind=PLANET orbit=695 radius=1.4999718538392695 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1194906_-4530040_6629503 kind=GAS_GIANT orbit=3014 radius=4.943021985795619 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1194906_-4530040_6629503 kind=MOON orbit=3014 radius=0.21118755057036187 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1194906_-4530040_6629503 kind=MOON orbit=3014 radius=0.6653742939458489 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1195455_-4529982_6629794 kind=MOON orbit=6065 radius=0.25884471688542643 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1195455_-4529982_6629794 kind=MOON orbit=6065 radius=0.680236793586125 starId=-962147133 frame=false + body 1194346_-4530025_6629562 1195455_-4529982_6629794 kind=PLANET orbit=6065 radius=1.081029895238314 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1195499_-4530033_6628603 kind=PLANET orbit=8022 radius=0.4745120039726671 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1195668_-4530126_6632475 kind=GAS_GIANT orbit=17115 radius=7.003259256804836 starId=-962147133 frame=true + body 1194346_-4530025_6629562 1196104_-4530132_6637414 kind=ASTEROID_BELT orbit=43032 radius=0.0 starId=-962147133 frame=true + body 1756923_-1971171_998832 1756304_-1971195_999471 kind=MOON orbit=4759 radius=0.3850177207349733 starId=-1811991613 frame=false + body 1756923_-1971171_998832 1756304_-1971195_999471 kind=MOON orbit=4759 radius=0.4933377071413044 starId=-1811991613 frame=false + body 1756923_-1971171_998832 1756304_-1971195_999471 kind=PLANET orbit=4759 radius=0.36539458876974434 starId=-1811991613 frame=true + body 1756923_-1971171_998832 1756802_-1971224_997414 kind=ASTEROID_BELT orbit=7614 radius=0.0 starId=-1811991613 frame=true + body 1756923_-1971171_998832 1756895_-1971172_998845 kind=PLANET orbit=163 radius=0.2058604344637433 starId=-1811991613 frame=true + body 1756923_-1971171_998832 1756923_-1971171_998832 kind=STAR orbit=0 radius=0.0 starId=-1811991613 frame=true + body 1756923_-1971171_998832 1756927_-1971170_998853 kind=PLANET orbit=114 radius=0.6445989983146653 starId=-1811991613 frame=true + body 1756923_-1971171_998832 1756928_-1971171_998828 kind=MOON orbit=35 radius=0.48316917319884634 starId=-1811991613 frame=false + body 1756923_-1971171_998832 1756928_-1971171_998828 kind=PLANET orbit=35 radius=0.6410672902404898 starId=-1811991613 frame=true + body 1756923_-1971171_998832 1757015_-1971171_998790 kind=STAR orbit=540 radius=118.307769895792 starId=-1811991614 frame=true + body 1756923_-1971171_998832 1757193_-1971162_999102 kind=PLANET orbit=2041 radius=1.3665356678264369 starId=-1811991613 frame=true + body 1836710_7713193_-3440196 1836710_7713193_-3440196 kind=ROGUE_PLANET orbit=0 radius=0.9935421843295233 starId=-1441927509 frame=true + body 2469282_4416743_-723947 2469276_4416743_-723937 kind=ASTEROID_BELT orbit=65 radius=0.0 starId=-776084721 frame=true + body 2469282_4416743_-723947 2469282_4416743_-723942 kind=PLANET orbit=26 radius=0.8567827527241039 starId=-776084721 frame=true + body 2469282_4416743_-723947 2469282_4416743_-723947 kind=STAR orbit=0 radius=0.0 starId=-776084721 frame=true + body 2469282_4416743_-723947 2469283_4416743_-723947 kind=PLANET orbit=7 radius=1.6637654823213972 starId=-776084721 frame=true + body 2469282_4416743_-723947 2469303_4416743_-723951 kind=GAS_GIANT orbit=117 radius=8.252127224356407 starId=-776084721 frame=true + body 2469282_4416743_-723947 2469310_4416743_-723868 kind=ASTEROID_BELT orbit=446 radius=0.0 starId=-776084721 frame=true + body 2469282_4416743_-723947 2469310_4416744_-723991 kind=PLANET orbit=279 radius=0.3860558158888115 starId=-776084721 frame=true + body 2948486_1502429_3559523 2948486_1502429_3559523 kind=ROGUE_PLANET orbit=0 radius=2.4054665038276375 starId=-875095761 frame=true + body 3510787_4170368_5776814 3510787_4170368_5776814 kind=MOON orbit=0 radius=1.8026104261664744 starId=-205539377 frame=false + body 3510787_4170368_5776814 3510787_4170368_5776814 kind=ROGUE_PLANET orbit=0 radius=1.1577006008040243 starId=-205539377 frame=true + body 3709337_6524042_9099103 3709202_6524034_9098955 kind=ASTEROID_BELT orbit=1073 radius=0.0 starId=-1795014233 frame=true + body 3709337_6524042_9099103 3709286_6524045_9099143 kind=MOON orbit=348 radius=0.24336564597805202 starId=-1795014233 frame=false + body 3709337_6524042_9099103 3709286_6524045_9099143 kind=MOON orbit=348 radius=0.24796229063275702 starId=-1795014233 frame=false + body 3709337_6524042_9099103 3709286_6524045_9099143 kind=PLANET orbit=348 radius=0.20215208272759683 starId=-1795014233 frame=true + body 3709337_6524042_9099103 3709337_6524042_9099103 kind=STAR orbit=0 radius=0.0 starId=-1795014233 frame=true + body 3709337_6524042_9099103 3709337_6524042_9099104 kind=PLANET orbit=8 radius=0.2024093892049266 starId=-1795014233 frame=true + body 3709337_6524042_9099103 3709343_6524042_9099110 kind=STAR orbit=52 radius=69.71192023336887 starId=-1795014234 frame=true + body 3709337_6524042_9099103 3709393_6524040_9098991 kind=MOON orbit=671 radius=0.21707797232613227 starId=-1795014233 frame=false + body 3709337_6524042_9099103 3709393_6524040_9098991 kind=MOON orbit=671 radius=0.30680587463262243 starId=-1795014233 frame=false + body 3709337_6524042_9099103 3709393_6524040_9098991 kind=PLANET orbit=671 radius=0.317794798051077 starId=-1795014233 frame=true + body 4213602_-4440402_-2844594 4213602_-4440402_-2844594 kind=MOON orbit=0 radius=0.3460685170641377 starId=-1923362853 frame=false + body 4213602_-4440402_-2844594 4213602_-4440402_-2844594 kind=ROGUE_PLANET orbit=0 radius=0.2143152268239313 starId=-1923362853 frame=true + body 4595242_8109101_2801656 4595242_8109101_2801656 kind=MOON orbit=0 radius=0.8302485764437313 starId=-22049925 frame=false + body 4595242_8109101_2801656 4595242_8109101_2801656 kind=ROGUE_PLANET orbit=0 radius=1.614857845425093 starId=-22049925 frame=true + body 6380022_2179216_8117900 6380022_2179216_8117900 kind=MOON orbit=0 radius=0.6122935598736741 starId=-1874711009 frame=false + body 6380022_2179216_8117900 6380022_2179216_8117900 kind=MOON orbit=0 radius=1.7700720105524679 starId=-1874711009 frame=false + body 6380022_2179216_8117900 6380022_2179216_8117900 kind=ROGUE_PLANET orbit=0 radius=2.4803562719196868 starId=-1874711009 frame=true + body 6473135_-2293275_-391540 6473135_-2293275_-391540 kind=ROGUE_PLANET orbit=0 radius=1.2124545908211712 starId=-431200341 frame=true + body 7105709_232763_-3168650 7105699_232763_-3168652 kind=MOON orbit=54 radius=0.21559689240507918 starId=-1067008965 frame=false + body 7105709_232763_-3168650 7105699_232763_-3168652 kind=MOON orbit=54 radius=0.3575168734752142 starId=-1067008965 frame=false + body 7105709_232763_-3168650 7105699_232763_-3168652 kind=PLANET orbit=54 radius=2.1885660950039196 starId=-1067008965 frame=true + body 7105709_232763_-3168650 7105707_232764_-3168669 kind=MOON orbit=103 radius=0.23407286413800515 starId=-1067008965 frame=false + body 7105709_232763_-3168650 7105707_232764_-3168669 kind=PLANET orbit=103 radius=1.7951422258541543 starId=-1067008965 frame=true + body 7105709_232763_-3168650 7105708_232763_-3168647 kind=STAR orbit=16 radius=100.66049123346805 starId=-1067008966 frame=true + body 7105709_232763_-3168650 7105709_232763_-3168650 kind=STAR orbit=0 radius=0.0 starId=-1067008965 frame=true + body 7105709_232763_-3168650 7105776_232759_-3168549 kind=PLANET orbit=646 radius=0.4394546005763268 starId=-1067008965 frame=true + body 7105709_232763_-3168650 7105810_232761_-3168517 kind=ASTEROID_BELT orbit=894 radius=0.0 starId=-1067008965 frame=true + body 7105709_232763_-3168650 7105921_232772_-3169082 kind=ASTEROID_BELT orbit=2576 radius=0.0 starId=-1067008965 frame=true + body 7105709_232763_-3168650 7106003_232766_-3168585 kind=GAS_GIANT orbit=1610 radius=4.978444629240493 starId=-1067008965 frame=true + body 7105709_232763_-3168650 7106003_232766_-3168585 kind=MOON orbit=1610 radius=0.20965217397446498 starId=-1067008965 frame=false + body 7105709_232763_-3168650 7106003_232766_-3168585 kind=MOON orbit=1610 radius=0.34767981578720686 starId=-1067008965 frame=false + body 7105709_232763_-3168650 7106003_232766_-3168585 kind=MOON orbit=1610 radius=0.3970400353089477 starId=-1067008965 frame=false + body 7105709_232763_-3168650 7106003_232766_-3168585 kind=MOON orbit=1610 radius=0.7155332025822625 starId=-1067008965 frame=false + body 7696665_9003901_1665902 7696665_9003901_1665902 kind=ROGUE_PLANET orbit=0 radius=0.5411770116500192 starId=-1655956185 frame=true + body 7791799_600353_1229440 7791799_600353_1229440 kind=MOON orbit=0 radius=1.9632924173437158 starId=-1694756545 frame=false + body 7791799_600353_1229440 7791799_600353_1229440 kind=ROGUE_PLANET orbit=0 radius=2.227881040854203 starId=-1694756545 frame=true + body 7795016_5356799_-122233 7794967_5356796_-122271 kind=PLANET orbit=330 radius=2.0259534223080053 starId=-1185086445 frame=true + body 7795016_5356799_-122233 7795016_5356799_-122229 kind=ASTEROID_BELT orbit=20 radius=0.0 starId=-1185086445 frame=true + body 7795016_5356799_-122233 7795016_5356799_-122233 kind=STAR orbit=0 radius=0.0 starId=-1185086445 frame=true + body 7795016_5356799_-122233 7795017_5356799_-122235 kind=MOON orbit=10 radius=0.36204631187776787 starId=-1185086445 frame=false + body 7795016_5356799_-122233 7795017_5356799_-122235 kind=PLANET orbit=10 radius=1.5197635756613461 starId=-1185086445 frame=true + body 7795016_5356799_-122233 7795022_5356799_-122236 kind=GAS_GIANT orbit=37 radius=4.83698459834699 starId=-1185086445 frame=true + body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.24968217148905664 starId=-1185086445 frame=false + body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.3569127342268691 starId=-1185086445 frame=false + body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.4627425908852818 starId=-1185086445 frame=false + body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.6312409026920827 starId=-1185086445 frame=false + body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.7410762898997516 starId=-1185086445 frame=false + body 7795016_5356799_-122233 7795092_5356798_-122169 kind=ASTEROID_BELT orbit=528 radius=0.0 starId=-1185086445 frame=true + body 8451398_6964684_8229128 8451398_6964684_8229128 kind=ROGUE_PLANET orbit=0 radius=1.0670964107230607 starId=-1402445041 frame=true + body 9167587_-4662890_1302687 9167587_-4662890_1302687 kind=MOON orbit=0 radius=2.043402615907837 starId=-1312561 frame=false + body 9167587_-4662890_1302687 9167587_-4662890_1302687 kind=ROGUE_PLANET orbit=0 radius=2.2012889250090324 starId=-1312561 frame=true + body 9170317_-4479146_7116367 9164861_-4479305_7117793 kind=ASTEROID_BELT orbit=30166 radius=0.0 starId=-274851001 frame=true + body 9170317_-4479146_7116367 9169176_-4479116_7113031 kind=MOON orbit=18854 radius=0.28236931695299683 starId=-274851001 frame=false + body 9170317_-4479146_7116367 9169176_-4479116_7113031 kind=MOON orbit=18854 radius=0.5826993168629868 starId=-274851001 frame=false + body 9170317_-4479146_7116367 9169176_-4479116_7113031 kind=PLANET orbit=18854 radius=1.0611997784852054 starId=-274851001 frame=true + body 9170317_-4479146_7116367 9169400_-4479146_7116327 kind=STAR orbit=4907 radius=93.78765245497227 starId=-274851002 frame=true + body 9170317_-4479146_7116367 9170250_-4479144_7116312 kind=PLANET orbit=461 radius=0.32472363860513237 starId=-274851001 frame=true + body 9170317_-4479146_7116367 9170317_-4479146_7116367 kind=STAR orbit=0 radius=0.0 starId=-274851001 frame=true + derived -146732_3144538_9058898 -146732_3144538_9058898 type=ice mass=0.022867406045862786 radius=0.365137001300595 gravity=17 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=31987 metallicity=0.9305060662680157 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1990844_4373138_-3056240 -1990792_4373137_-3056198 type=ice mass=2.940584609827984 radius=1.2959853165262225 gravity=175 pressure=1600 tempK=104 oxygen=false locked=false rings=false rotation=9542 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1990844_4373138_-3056240 -1990836_4373138_-3056240 type=superearth mass=16.187528817886992 radius=2.0020815425545506 gravity=400 pressure=1600 tempK=336 oxygen=false locked=true rings=false rotation=15881 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1990844_4373138_-3056240 -1990844_4373138_-3056240 type=lava mass=3.2718283342246335 radius=1.4817161256622684 gravity=149 pressure=42 tempK=1068 oxygen=false locked=true rings=false rotation=84459 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1990844_4373138_-3056240 -1990845_4373138_-3056241 type=barren mass=0.02576612260246133 radius=0.3898083310932754 gravity=17 pressure=0 tempK=335 oxygen=false locked=true rings=false rotation=29642 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1990844_4373138_-3056240 -1990851_4373138_-3056227 type=ice mass=0.04180064524858778 radius=0.3993429730787532 gravity=26 pressure=4 tempK=97 oxygen=false locked=false rings=false rotation=21004 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1990844_4373138_-3056240 -1990857_4373137_-3056263 type=icegiant mass=89.41371358234053 radius=6.336033623503909 gravity=223 pressure=1600 tempK=172 oxygen=false locked=false rings=false rotation=6980 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1990844_4373138_-3056240 -1990868_4373135_-3056136 type=barren mass=0.002204749524119052 radius=0.2030805557303468 gravity=5 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=16289 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2174817_-2291967_261255 -2174817_-2291967_261255 type=ice mass=2.313292911863274 radius=1.3479949218897396 gravity=127 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=20830 metallicity=0.6050614095134093 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3680127_6497425_-2974555 -3680127_6497425_-2974555 type=ice mass=8.756134954746999 radius=1.7790054869434075 gravity=277 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=33700 metallicity=0.7822814915510611 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3867498_449288_4118676 -3867498_449288_4118676 type=ice mass=0.00906422581450899 radius=0.2746107364620147 gravity=12 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=15457 metallicity=0.82564227960862 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4373024_7511005_1816766 -4370934_7510969_1815501 type=ice mass=2.33441507382284 radius=1.2856364704578582 gravity=141 pressure=1600 tempK=73 oxygen=false locked=false rings=false rotation=8434 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4373024_7511005_1816766 -4372159_7511003_1818025 type=superearth mass=3.4310841957190874 radius=1.3472896720527072 gravity=189 pressure=1600 tempK=106 oxygen=false locked=false rings=false rotation=11459 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4373024_7511005_1816766 -4372915_7511005_1817033 type=superearth mass=4.257285540746229 radius=1.5094921649735527 gravity=187 pressure=1600 tempK=244 oxygen=false locked=false rings=false rotation=8599 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4373024_7511005_1816766 -4372992_7511006_1816751 type=barren mass=0.17831566243517052 radius=0.6586797028292382 gravity=41 pressure=6 tempK=328 oxygen=false locked=false rings=false rotation=49992 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4373024_7511005_1816766 -4373006_7511005_1816753 type=barren mass=0.1292392825281645 radius=0.6040809505464881 gravity=35 pressure=1 tempK=413 oxygen=false locked=false rings=false rotation=60605 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4373024_7511005_1816766 -4373024_7511005_1816766 type=lava mass=0.06952416485465254 radius=0.4798418385120569 gravity=30 pressure=0 tempK=4539 oxygen=false locked=true rings=false rotation=24606 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4373024_7511005_1816766 -4373066_7511005_1816718 type=gasgiant mass=22.26080765139372 radius=3.461531189557112 gravity=186 pressure=1600 tempK=477 oxygen=false locked=false rings=true rotation=5729 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4752099_9055058_8335985 -4752099_9055058_8335985 type=ice mass=0.005085643854145261 radius=0.24557082150399945 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=61960 metallicity=0.6851999470392627 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4890357_-2814414_6240914 -4890357_-2814414_6240914 type=ice mass=6.75576181954006 radius=1.6732860210523022 gravity=241 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=74241 metallicity=0.8743399901543765 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -670621_-4121674_-2164509 type=ice mass=8.282097933741516 radius=1.8571244196111838 gravity=240 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=10908 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -670902_-4121657_-2164531 type=icegiant mass=118.13494379261064 radius=7.1517935902824386 gravity=231 pressure=1600 tempK=142 oxygen=false locked=false rings=true rotation=8687 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -670986_-4121661_-2164629 type=barren mass=0.0025692429228558206 radius=0.20105961349957216 gravity=6 pressure=0 tempK=194 oxygen=false locked=false rings=false rotation=72140 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -670989_-4121660_-2164613 type=greenhouse mass=24.65940033203534 radius=2.341480090625241 gravity=400 pressure=1600 tempK=915 oxygen=false locked=true rings=false rotation=60344 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -670991_-4121660_-2164613 type=lava mass=0.005455754543576298 radius=0.23410640865996032 gravity=10 pressure=0 tempK=1858 oxygen=false locked=true rings=false rotation=93940 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -670996_-4121660_-2164616 type=barren mass=0.009047298081132884 radius=0.27867397404199995 gravity=12 pressure=0 tempK=331 oxygen=false locked=true rings=false rotation=48589 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -671002_-4121659_-2164570 type=superearth mass=7.9096413631715885 radius=1.8533917606053845 gravity=230 pressure=1600 tempK=254 oxygen=false locked=false rings=false rotation=8355 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -671003_-4121660_-2164619 type=barren mass=0.13073198117259513 radius=0.5699188917970125 gravity=40 pressure=2 tempK=219 oxygen=false locked=false rings=false rotation=10344 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -671008_-4121660_-2164588 type=gasgiant mass=33.75495820252818 radius=4.148350959171349 gravity=196 pressure=1600 tempK=283 oxygen=false locked=false rings=true rotation=10260 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -670991_-4121660_-2164613 -671165_-4121668_-2164448 type=ice mass=4.397623655143227 radius=1.518213839049826 gravity=191 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=78861 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1191593_-4530197_6633767 type=ice mass=0.029366891031306075 radius=0.3710815068256704 gravity=21 pressure=8 tempK=38 oxygen=false locked=false rings=false rotation=58853 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1194117_-4530031_6629817 type=gasgiant mass=19.392650674542935 radius=3.260041612564983 gravity=182 pressure=1600 tempK=346 oxygen=false locked=false rings=false rotation=10334 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1194162_-4530030_6629610 type=barren mass=0.22033439440422145 radius=0.6761236797406471 gravity=48 pressure=9 tempK=238 oxygen=false locked=false rings=false rotation=9477 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1194189_-4530017_6629650 type=superearth mass=11.798299735529016 radius=1.910283521314024 gravity=323 pressure=1600 tempK=520 oxygen=false locked=false rings=false rotation=44876 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1194309_-4530025_6629559 type=desert mass=0.10652823903410986 radius=0.5175670411566737 gravity=40 pressure=0 tempK=511 oxygen=false locked=false rings=false rotation=6003 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1194313_-4530027_6629512 type=superearth mass=13.406205618009157 radius=1.986963906941915 gravity=340 pressure=1600 tempK=898 oxygen=false locked=false rings=false rotation=10515 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1194346_-4530025_6629562 type=unclassified mass=0.7358493099176111 radius=0.9648973832295691 gravity=79 pressure=0 tempK=7177 oxygen=false locked=true rings=false rotation=47233 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1194351_-4530025_6629565 type=lava mass=5.989648456196548 radius=1.6243169204923151 gravity=227 pressure=107 tempK=1570 oxygen=false locked=true rings=false rotation=36181 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1194475_-4530022_6629580 type=greenhouse mass=3.521637322683175 radius=1.4999718538392695 gravity=157 pressure=1600 tempK=473 oxygen=false locked=false rings=false rotation=40417 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1194906_-4530040_6629503 type=gasgiant mass=50.51342206119997 radius=4.943021985795619 gravity=207 pressure=1600 tempK=270 oxygen=false locked=false rings=true rotation=5691 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1195455_-4529982_6629794 type=ice mass=1.540739662341268 radius=1.081029895238314 gravity=132 pressure=1600 tempK=180 oxygen=false locked=false rings=true rotation=29964 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1195499_-4530033_6628603 type=barren mass=0.07638558844916687 radius=0.4745120039726671 gravity=34 pressure=21 tempK=84 oxygen=false locked=false rings=false rotation=83853 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1195668_-4530126_6632475 type=gasgiant mass=112.56786087925825 radius=7.003259256804836 gravity=230 pressure=1600 tempK=113 oxygen=false locked=false rings=false rotation=6319 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1194346_-4530025_6629562 1196104_-4530132_6637414 type=ice mass=21.669836792937 radius=2.1679276523685638 gravity=400 pressure=1600 tempK=67 oxygen=false locked=false rings=false rotation=21439 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1756923_-1971171_998832 1756304_-1971195_999471 type=ice mass=0.0270649648764436 radius=0.36539458876974434 gravity=20 pressure=4 tempK=74 oxygen=false locked=false rings=false rotation=21192 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1756923_-1971171_998832 1756802_-1971224_997414 type=ice mass=0.9932954039372163 radius=0.9585396474024162 gravity=108 pressure=1600 tempK=132 oxygen=false locked=false rings=false rotation=12846 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1756923_-1971171_998832 1756895_-1971172_998845 type=barren mass=0.002677086174508008 radius=0.2058604344637433 gravity=6 pressure=0 tempK=289 oxygen=false locked=false rings=false rotation=59451 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1756923_-1971171_998832 1756923_-1971171_998832 type=lava mass=10.54939105687714 radius=1.9434421937694413 gravity=279 pressure=51 tempK=2836 oxygen=false locked=true rings=false rotation=70186 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1756923_-1971171_998832 1756927_-1971170_998853 type=desert mass=0.1919300415202249 radius=0.6445989983146653 gravity=46 pressure=2 tempK=296 oxygen=false locked=false rings=false rotation=7914 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1756923_-1971171_998832 1756928_-1971171_998828 type=barren mass=0.19517868947521144 radius=0.6410672902404898 gravity=47 pressure=1 tempK=487 oxygen=false locked=true rings=false rotation=13411 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1756923_-1971171_998832 1757015_-1971171_998790 type=desert mass=0.4602241031797454 radius=0.8145284093856098 gravity=69 pressure=24 tempK=216 oxygen=false locked=false rings=false rotation=9149 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1756923_-1971171_998832 1757193_-1971162_999102 type=exotic mass=2.749365528960417 radius=1.3665356678264369 gravity=147 pressure=1600 tempK=290 oxygen=false locked=false rings=false rotation=7602 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1836710_7713193_-3440196 1836710_7713193_-3440196 type=ice mass=0.967637822427201 radius=0.9935421843295233 gravity=98 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=57631 metallicity=1.5477966882660805 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2469282_4416743_-723947 2469276_4416743_-723937 type=gasgiant mass=55.70351201841058 radius=5.157749853921637 gravity=209 pressure=1600 tempK=216 oxygen=false locked=false rings=true rotation=6897 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2469282_4416743_-723947 2469282_4416743_-723942 type=ice mass=0.4300884439870668 radius=0.8567827527241039 gravity=59 pressure=94 tempK=159 oxygen=false locked=true rings=false rotation=38289 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2469282_4416743_-723947 2469282_4416743_-723947 type=barren mass=0.4517766972918707 radius=0.863002096992908 gravity=61 pressure=1 tempK=894 oxygen=false locked=true rings=false rotation=39145 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2469282_4416743_-723947 2469283_4416743_-723947 type=superearth mass=5.538914624136937 radius=1.6637654823213972 gravity=200 pressure=1134 tempK=658 oxygen=false locked=true rings=false rotation=12253 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2469282_4416743_-723947 2469303_4416743_-723951 type=gasgiant mass=164.1820025391923 radius=8.252127224356407 gravity=241 pressure=1600 tempK=161 oxygen=false locked=false rings=true rotation=9187 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2469282_4416743_-723947 2469310_4416743_-723868 type=ice mass=0.39810198730358975 radius=0.7379293830014126 gravity=73 pressure=491 tempK=58 oxygen=false locked=false rings=false rotation=13184 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2469282_4416743_-723947 2469310_4416744_-723991 type=barren mass=0.022462386186608514 radius=0.3860558158888115 gravity=15 pressure=1 tempK=53 oxygen=false locked=false rings=false rotation=7895 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2948486_1502429_3559523 2948486_1502429_3559523 type=ice mass=21.212812738793687 radius=2.4054665038276375 gravity=367 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=37771 metallicity=1.4470347033190816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3510787_4170368_5776814 3510787_4170368_5776814 type=ice mass=1.5534085098665662 radius=1.1577006008040243 gravity=116 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=27402 metallicity=1.3243201471707986 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3709337_6524042_9099103 3709202_6524034_9098955 type=barren mass=0.002930641312528724 radius=0.20200695258004708 gravity=7 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=15666 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3709337_6524042_9099103 3709286_6524045_9099143 type=ice mass=0.003314115116235893 radius=0.20215208272759683 gravity=8 pressure=0 tempK=68 oxygen=false locked=false rings=false rotation=70775 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3709337_6524042_9099103 3709337_6524042_9099103 type=barren mass=0.0023816022881015756 radius=0.2008675570768562 gravity=6 pressure=0 tempK=866 oxygen=false locked=true rings=false rotation=9655 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3709337_6524042_9099103 3709337_6524042_9099104 type=barren mass=0.002568901725669564 radius=0.2024093892049266 gravity=6 pressure=0 tempK=321 oxygen=false locked=true rings=false rotation=7242 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3709337_6524042_9099103 3709343_6524042_9099110 type=greenhouse mass=26.03102594223896 radius=2.309122467818324 gravity=400 pressure=1600 tempK=304 oxygen=false locked=false rings=false rotation=10673 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3709337_6524042_9099103 3709393_6524040_9098991 type=ice mass=0.01208302049187343 radius=0.317794798051077 gravity=12 pressure=3 tempK=49 oxygen=false locked=false rings=false rotation=16818 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4213602_-4440402_-2844594 4213602_-4440402_-2844594 type=barren mass=0.003008765886724551 radius=0.2143152268239313 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=69971 metallicity=0.7299135708064022 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4595242_8109101_2801656 4595242_8109101_2801656 type=ice mass=7.252268911685418 radius=1.614857845425093 gravity=278 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=6343 metallicity=0.8659153883644486 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6380022_2179216_8117900 6380022_2179216_8117900 type=superearth mass=23.61670682531038 radius=2.4803562719196868 gravity=384 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=83230 metallicity=0.5776707870302991 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6473135_-2293275_-391540 6473135_-2293275_-391540 type=ice mass=1.9964756602479061 radius=1.2124545908211712 gravity=136 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=9299 metallicity=1.104939030674866 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7105709_232763_-3168650 7105699_232763_-3168652 type=greenhouse mass=22.49121722974076 radius=2.1885660950039196 gravity=400 pressure=1600 tempK=603 oxygen=false locked=false rings=false rotation=17608 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7105709_232763_-3168650 7105707_232764_-3168669 type=superearth mass=7.147461200654439 radius=1.7951422258541543 gravity=222 pressure=1600 tempK=571 oxygen=false locked=false rings=false rotation=9877 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7105709_232763_-3168650 7105708_232763_-3168647 type=desert mass=2.181410071978156 radius=1.307253044809006 gravity=128 pressure=38 tempK=569 oxygen=false locked=true rings=false rotation=24769 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7105709_232763_-3168650 7105709_232763_-3168650 type=lava mass=0.004534199803455963 radius=0.22708391874861367 gravity=9 pressure=0 tempK=1838 oxygen=false locked=true rings=false rotation=21113 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7105709_232763_-3168650 7105776_232759_-3168549 type=barren mass=0.05003200502122312 radius=0.4394546005763268 gravity=26 pressure=4 tempK=107 oxygen=false locked=false rings=false rotation=56490 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7105709_232763_-3168650 7105810_232761_-3168517 type=barren mass=0.01515770663921861 radius=0.33398669745811155 gravity=14 pressure=1 tempK=91 oxygen=false locked=false rings=false rotation=74044 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7105709_232763_-3168650 7105921_232772_-3169082 type=icegiant mass=107.7729463921819 radius=6.871962609713998 gravity=228 pressure=1600 tempK=105 oxygen=false locked=false rings=true rotation=11956 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7105709_232763_-3168650 7106003_232766_-3168585 type=gasgiant mass=51.34987738801249 radius=4.978444629240493 gravity=207 pressure=1600 tempK=133 oxygen=false locked=false rings=true rotation=8750 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7696665_9003901_1665902 7696665_9003901_1665902 type=barren mass=0.09962695429765411 radius=0.5411770116500192 gravity=34 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=9596 metallicity=1.4134633815807982 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7791799_600353_1229440 7791799_600353_1229440 type=superearth mass=19.254385948976793 radius=2.227881040854203 gravity=388 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=19160 metallicity=1.0481893678631202 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7795016_5356799_-122233 7794967_5356796_-122271 type=ice mass=14.578610762625457 radius=2.0259534223080053 gravity=355 pressure=1600 tempK=93 oxygen=false locked=false rings=false rotation=13699 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7795016_5356799_-122233 7795016_5356799_-122229 type=gasgiant mass=219.14501235596202 radius=9.356001465066456 gravity=250 pressure=1600 tempK=403 oxygen=false locked=false rings=true rotation=7519 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7795016_5356799_-122233 7795016_5356799_-122233 type=lava mass=6.6310319663732535 radius=1.6508264151239065 gravity=243 pressure=317 tempK=1394 oxygen=false locked=true rings=false rotation=20348 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7795016_5356799_-122233 7795017_5356799_-122235 type=superearth mass=3.8406646125641988 radius=1.5197635756613461 gravity=166 pressure=890 tempK=535 oxygen=false locked=true rings=false rotation=7771 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7795016_5356799_-122233 7795022_5356799_-122236 type=gasgiant mass=48.05579480668342 radius=4.83698459834699 gravity=205 pressure=1600 tempK=296 oxygen=false locked=false rings=true rotation=12224 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7795016_5356799_-122233 7795092_5356798_-122169 type=barren mass=0.0033540793281609635 radius=0.2039556489264513 gravity=8 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=18724 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8451398_6964684_8229128 8451398_6964684_8229128 type=ice mass=1.2382410107593795 radius=1.0670964107230607 gravity=109 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=14466 metallicity=0.39658577524042765 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9167587_-4662890_1302687 9167587_-4662890_1302687 type=ice mass=21.63652698948589 radius=2.2012889250090324 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=31418 metallicity=0.3755972598382573 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9170317_-4479146_7116367 9164861_-4479305_7117793 type=ice mass=0.21076566380930786 radius=0.6213603213580874 gravity=55 pressure=209 tempK=51 oxygen=false locked=false rings=false rotation=24568 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9170317_-4479146_7116367 9169176_-4479116_7113031 type=ice mass=1.5325568551099056 radius=1.0611997784852054 gravity=136 pressure=1600 tempK=108 oxygen=false locked=false rings=false rotation=54585 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9170317_-4479146_7116367 9169400_-4479146_7116327 type=ice mass=0.024929585238438568 radius=0.35868099901754724 gravity=19 pressure=3 tempK=94 oxygen=false locked=false rings=false rotation=79964 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9170317_-4479146_7116367 9170250_-4479144_7116312 type=barren mass=0.014119376228983096 radius=0.32472363860513237 gravity=13 pressure=0 tempK=376 oxygen=false locked=false rings=false rotation=25222 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9170317_-4479146_7116367 9170317_-4479146_7116367 type=unclassified mass=20.97066498348173 radius=2.205923574177095 gravity=400 pressure=9 tempK=7626 oxygen=false locked=true rings=false rotation=8222 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] + system -146732_3144538_9058898 id=-478929905 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless + system -1990844_4373138_-3056240 id=-1524375109 kind=STAR name=PGS--5002361.0.-5002361 starTemp=40 starSize=0.9608185887336731 + system -2174817_-2291967_261255 id=-1472462165 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless + system -3680127_6497425_-2974555 id=-1983124585 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless + system -3867498_449288_4118676 id=-1943963613 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless + system -4373024_7511005_1816766 id=-110594437 kind=STAR name=PGS--5002361.5002361.0 starTemp=150 starSize=1.2341642379760742 + system -4752099_9055058_8335985 id=-392839469 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless + system -4890357_-2814414_6240914 id=-759681393 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless + system -670991_-4121660_-2164613 id=-262321917 kind=STAR name=PGS--5002361.-5002361.-5002361 starTemp=70 starSize=0.9494102001190186 + system 1194346_-4530025_6629562 id=-962147133 kind=STAR name=PGS-0.-5002361.5002361 starTemp=220 starSize=1.6262410879135132 + system 1756923_-1971171_998832 id=-1811991613 kind=STAR name=PGS-0.-5002361.0 starTemp=100 starSize=1.0837022066116333 + system 1836710_7713193_-3440196 id=-1441927509 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless + system 2469282_4416743_-723947 id=-776084721 kind=STAR name=PGS-0.0.-5002361 starTemp=40 starSize=0.6808884739875793 + system 2948486_1502429_3559523 id=-875095761 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 3510787_4170368_5776814 id=-205539377 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless + system 3709337_6524042_9099103 id=-1795014233 kind=STAR name=PGS-0.5002361.5002361 starTemp=40 starSize=0.6385629773139954 + system 4213602_-4440402_-2844594 id=-1923362853 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless + system 4595242_8109101_2801656 id=-22049925 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless + system 6380022_2179216_8117900 id=-1874711009 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless + system 6473135_-2293275_-391540 id=-431200341 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless + system 7105709_232763_-3168650 id=-1067008965 kind=STAR name=PGS-5002361.0.-5002361 starTemp=70 starSize=0.9220526814460754 + system 7696665_9003901_1665902 id=-1655956185 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless + system 7791799_600353_1229440 id=-1694756545 kind=ROGUE_PLANET name=PGR-5002361.0.0 starless + system 7795016_5356799_-122233 id=-1185086445 kind=STAR name=PGS-5002361.5002361.-5002361 starTemp=40 starSize=0.7262133359909058 + system 8451398_6964684_8229128 id=-1402445041 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless + system 9167587_-4662890_1302687 id=-1312561 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless + system 9170317_-4479146_7116367 id=-274851001 kind=STAR name=PGS-5002361.-5002361.5002361 starTemp=220 starSize=1.8361765146255493 +seed 42 systems=27 + body -147990_-4259982_-3782514 -147990_-4259982_-3782514 kind=MOON orbit=0 radius=1.8658608999383741 starId=-1021692653 frame=false + body -147990_-4259982_-3782514 -147990_-4259982_-3782514 kind=ROGUE_PLANET orbit=0 radius=0.2447237286529341 starId=-1021692653 frame=true + body -1763249_4241372_4386755 -1763249_4241372_4386755 kind=ROGUE_PLANET orbit=0 radius=0.32767939488983133 starId=-1507112769 frame=true + body -1811609_-4167359_4728900 -1811609_-4167359_4728900 kind=MOON orbit=0 radius=0.8636213541980484 starId=-1319680873 frame=false + body -1811609_-4167359_4728900 -1811609_-4167359_4728900 kind=MOON orbit=0 radius=1.3583852306221664 starId=-1319680873 frame=false + body -1811609_-4167359_4728900 -1811609_-4167359_4728900 kind=ROGUE_PLANET orbit=0 radius=1.8824394321228168 starId=-1319680873 frame=true + body -2114326_9425508_6200501 -2114326_9425508_6200501 kind=ROGUE_PLANET orbit=0 radius=1.0893681258617856 starId=-525238425 frame=true + body -2550052_6994691_-1786506 -2550052_6994691_-1786506 kind=ROGUE_PLANET orbit=0 radius=0.32882272714498073 starId=-1940610717 frame=true + body -2559146_-594813_8660842 -2559146_-594813_8660842 kind=ROGUE_PLANET orbit=0 radius=1.2175511349308201 starId=-322493161 frame=true + body -3234634_7878403_530638 -3234634_7878403_530638 kind=MOON orbit=0 radius=0.5704338346689674 starId=-782417921 frame=false + body -3234634_7878403_530638 -3234634_7878403_530638 kind=ROGUE_PLANET orbit=0 radius=1.3180441026495238 starId=-782417921 frame=true + body -4729070_1810660_-2040913 -4729070_1810660_-2040913 kind=ROGUE_PLANET orbit=0 radius=1.2653982081483837 starId=-1026598645 frame=true + body -517007_1928357_7786828 -517007_1928357_7786828 kind=ROGUE_PLANET orbit=0 radius=1.2632316197428803 starId=-819120817 frame=true + body 2446288_9823217_1140272 2446221_9823217_1140222 kind=STAR orbit=445 radius=67.32509275317192 starId=-677694142 frame=true + body 2446288_9823217_1140272 2446277_9823217_1140255 kind=GAS_GIANT orbit=108 radius=7.526351165166979 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446277_9823217_1140255 kind=MOON orbit=108 radius=0.22381749339605467 starId=-677694141 frame=false + body 2446288_9823217_1140272 2446277_9823217_1140255 kind=MOON orbit=108 radius=0.5067698734179844 starId=-677694141 frame=false + body 2446288_9823217_1140272 2446282_9823217_1140274 kind=MOON orbit=37 radius=0.7409829178572391 starId=-677694141 frame=false + body 2446288_9823217_1140272 2446282_9823217_1140274 kind=PLANET orbit=37 radius=1.3635217085174838 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446285_9823217_1140269 kind=PLANET orbit=23 radius=1.3471617398696347 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446287_9823217_1140272 kind=PLANET orbit=7 radius=1.1015474620849677 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446288_9823217_1140272 kind=STAR orbit=0 radius=0.0 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446289_9823217_1140273 kind=PLANET orbit=10 radius=0.2092017435866524 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446289_9823217_1140275 kind=PLANET orbit=18 radius=0.2114444291012643 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446290_9823217_1140262 kind=PLANET orbit=57 radius=0.2281630648172823 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446292_9823217_1140264 kind=ASTEROID_BELT orbit=49 radius=0.0 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446298_9823215_1140303 kind=ASTEROID_BELT orbit=172 radius=0.0 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446305_9823217_1140274 kind=GAS_GIANT orbit=89 radius=6.153023641353679 starId=-677694141 frame=true + body 2446288_9823217_1140272 2446305_9823217_1140274 kind=MOON orbit=89 radius=0.24758179107202047 starId=-677694141 frame=false + body 2446288_9823217_1140272 2446305_9823217_1140274 kind=MOON orbit=89 radius=0.2828896289074508 starId=-677694141 frame=false + body 2446288_9823217_1140272 2446305_9823217_1140274 kind=MOON orbit=89 radius=0.3633104289540722 starId=-677694141 frame=false + body 2446288_9823217_1140272 2446305_9823217_1140274 kind=MOON orbit=89 radius=0.3801950767827389 starId=-677694141 frame=false + body 331018_-2627546_4002550 331018_-2627546_4002550 kind=ROGUE_PLANET orbit=0 radius=0.648198320022664 starId=-778284213 frame=true + body 3810943_7578176_-1529346 3810832_7578170_-1529377 kind=MOON orbit=617 radius=0.20784045627308434 starId=-1781882117 frame=false + body 3810943_7578176_-1529346 3810832_7578170_-1529377 kind=MOON orbit=617 radius=0.5341977665142178 starId=-1781882117 frame=false + body 3810943_7578176_-1529346 3810832_7578170_-1529377 kind=PLANET orbit=617 radius=0.21061448925959111 starId=-1781882117 frame=true + body 3810943_7578176_-1529346 3810930_7578178_-1529283 kind=PLANET orbit=343 radius=1.1579452921819666 starId=-1781882117 frame=true + body 3810943_7578176_-1529346 3810940_7578176_-1529345 kind=STAR orbit=14 radius=82.01533210158348 starId=-1781882118 frame=true + body 3810943_7578176_-1529346 3810943_7578176_-1529346 kind=STAR orbit=0 radius=0.0 starId=-1781882117 frame=true + body 3810943_7578176_-1529346 3810948_7578177_-1529360 kind=PLANET orbit=80 radius=2.1277124765728215 starId=-1781882117 frame=true + body 3810943_7578176_-1529346 3810969_7578175_-1529352 kind=PLANET orbit=145 radius=0.4201376811063558 starId=-1781882117 frame=true + body 3810943_7578176_-1529346 3811028_7578212_-1530104 kind=ASTEROID_BELT orbit=4083 radius=0.0 starId=-1781882117 frame=true + body 3810943_7578176_-1529346 3811141_7578179_-1529254 kind=PLANET orbit=1167 radius=0.3932058876865541 starId=-1781882117 frame=true + body 3810943_7578176_-1529346 3811237_7578161_-1529722 kind=MOON orbit=2552 radius=0.21212185397109995 starId=-1781882117 frame=false + body 3810943_7578176_-1529346 3811237_7578161_-1529722 kind=MOON orbit=2552 radius=0.3343998307038777 starId=-1781882117 frame=false + body 3810943_7578176_-1529346 3811237_7578161_-1529722 kind=PLANET orbit=2552 radius=1.6686795831939623 starId=-1781882117 frame=true + body 4285126_880860_5633403 4285059_880873_5632810 kind=ASTEROID_BELT orbit=3190 radius=0.0 starId=-1076361445 frame=true + body 4285126_880860_5633403 4285073_880861_5633289 kind=PLANET orbit=672 radius=1.128421875313703 starId=-1076361445 frame=true + body 4285126_880860_5633403 4285102_880861_5633396 kind=ASTEROID_BELT orbit=133 radius=0.0 starId=-1076361445 frame=true + body 4285126_880860_5633403 4285109_880858_5633362 kind=GAS_GIANT orbit=240 radius=3.854805170021379 starId=-1076361445 frame=true + body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.20025483714909595 starId=-1076361445 frame=false + body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.2359910359192303 starId=-1076361445 frame=false + body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.3360418801675671 starId=-1076361445 frame=false + body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.3582314708170581 starId=-1076361445 frame=false + body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.7068934191067848 starId=-1076361445 frame=false + body 4285126_880860_5633403 4285126_880860_5633403 kind=STAR orbit=0 radius=0.0 starId=-1076361445 frame=true + body 4285126_880860_5633403 4285126_880860_5633405 kind=STAR orbit=9 radius=101.9993340432644 starId=-1076361446 frame=true + body 4285126_880860_5633403 4285131_880860_5633401 kind=PLANET orbit=30 radius=2.0812185825857674 starId=-1076361445 frame=true + body 4285126_880860_5633403 4285136_880859_5633399 kind=MOON orbit=57 radius=0.22397767735041765 starId=-1076361445 frame=false + body 4285126_880860_5633403 4285136_880859_5633399 kind=MOON orbit=57 radius=0.3211331002221319 starId=-1076361445 frame=false + body 4285126_880860_5633403 4285136_880859_5633399 kind=PLANET orbit=57 radius=1.401970044249525 starId=-1076361445 frame=true + body 4285126_880860_5633403 4285497_880855_5633365 kind=PLANET orbit=1994 radius=0.4319970136446857 starId=-1076361445 frame=true + body 4287194_-1179392_-2651040 4287194_-1179392_-2651040 kind=MOON orbit=0 radius=1.7342488163425542 starId=-493026021 frame=false + body 4287194_-1179392_-2651040 4287194_-1179392_-2651040 kind=ROGUE_PLANET orbit=0 radius=0.5675357443169351 starId=-493026021 frame=true + body 4540244_-2195284_6241886 4540243_-2195284_6241883 kind=PLANET orbit=15 radius=0.35933183620673187 starId=-1311232065 frame=true + body 4540244_-2195284_6241886 4540244_-2195284_6241886 kind=STAR orbit=0 radius=0.0 starId=-1311232065 frame=true + body 4540244_-2195284_6241886 4540250_-2195286_6241917 kind=PLANET orbit=167 radius=0.525683512535432 starId=-1311232065 frame=true + body 4540244_-2195284_6241886 4540258_-2195290_6241733 kind=PLANET orbit=820 radius=0.44478714057429125 starId=-1311232065 frame=true + body 4540244_-2195284_6241886 4540402_-2195292_6242073 kind=ASTEROID_BELT orbit=1312 radius=0.0 starId=-1311232065 frame=true + body 548615_3409698_-890024 548615_3409698_-890024 kind=MOON orbit=0 radius=1.0180941946589208 starId=-502144617 frame=false + body 548615_3409698_-890024 548615_3409698_-890024 kind=MOON orbit=0 radius=2.025383247329257 starId=-502144617 frame=false + body 548615_3409698_-890024 548615_3409698_-890024 kind=ROGUE_PLANET orbit=0 radius=0.9755138376471226 starId=-502144617 frame=true + body 5659038_1332946_1424772 5658972_1332942_1424881 kind=ASTEROID_BELT orbit=681 radius=0.0 starId=-1094984105 frame=true + body 5659038_1332946_1424772 5659002_1332946_1424807 kind=STAR orbit=265 radius=106.81875300943851 starId=-1094984106 frame=true + body 5659038_1332946_1424772 5659031_1332945_1424786 kind=PLANET orbit=84 radius=2.3821474285420066 starId=-1094984105 frame=true + body 5659038_1332946_1424772 5659038_1332946_1424767 kind=MOON orbit=28 radius=0.26461678783224585 starId=-1094984105 frame=false + body 5659038_1332946_1424772 5659038_1332946_1424767 kind=MOON orbit=28 radius=0.5694454525567392 starId=-1094984105 frame=false + body 5659038_1332946_1424772 5659038_1332946_1424767 kind=PLANET orbit=28 radius=0.5652156493519473 starId=-1094984105 frame=true + body 5659038_1332946_1424772 5659038_1332946_1424772 kind=STAR orbit=0 radius=0.0 starId=-1094984105 frame=true + body 5659038_1332946_1424772 5659146_1332945_1424974 kind=GAS_GIANT orbit=1226 radius=10.26830810198086 starId=-1094984105 frame=true + body 5659038_1332946_1424772 5659218_1332958_1425091 kind=ASTEROID_BELT orbit=1961 radius=0.0 starId=-1094984105 frame=true + body 6253955_-322304_1235104 6253955_-322304_1235104 kind=MOON orbit=0 radius=4.6400363601629975 starId=-1379053845 frame=false + body 6253955_-322304_1235104 6253955_-322304_1235104 kind=ROGUE_PLANET orbit=0 radius=0.4073729049004505 starId=-1379053845 frame=true + body 6326499_-4297175_-3049933 6326499_-4297175_-3049933 kind=ROGUE_PLANET orbit=0 radius=0.23300568312102693 starId=-335655149 frame=true + body 7518342_4618068_5122633 7518342_4618068_5122633 kind=ROGUE_PLANET orbit=0 radius=0.3709252755727711 starId=-1527090629 frame=true + body 783965_8560023_8900358 783965_8560023_8900358 kind=ROGUE_PLANET orbit=0 radius=1.2840791507608935 starId=-602652429 frame=true + body 8253893_-3146511_6379169 8253883_-3146511_6384549 kind=STAR orbit=28770 radius=73.8964213693142 starId=-1913306834 frame=true + body 8253893_-3146511_6379169 8253893_-3146511_6379169 kind=STAR orbit=0 radius=0.0 starId=-1913306833 frame=true + body 8253893_-3146511_6379169 8253895_-3146511_6379168 kind=PLANET orbit=13 radius=0.35018276058976605 starId=-1913306833 frame=true + body 8253893_-3146511_6379169 8253899_-3146511_6379172 kind=PLANET orbit=38 radius=1.9849839958996087 starId=-1913306833 frame=true + body 8253893_-3146511_6379169 8253909_-3146510_6379156 kind=PLANET orbit=112 radius=0.28522355165984403 starId=-1913306833 frame=true + body 8253893_-3146511_6379169 8253942_-3146510_6379142 kind=PLANET orbit=298 radius=2.1533492603769537 starId=-1913306833 frame=true + body 8253893_-3146511_6379169 8253955_-3146511_6379105 kind=ASTEROID_BELT orbit=476 radius=0.0 starId=-1913306833 frame=true + body 8259566_6483340_6516020 8259566_6483340_6516020 kind=ROGUE_PLANET orbit=0 radius=0.29300354320951977 starId=-373298525 frame=true + body 8396159_3733749_-457386 8396120_3733751_-457350 kind=MOON orbit=284 radius=0.4924311613292982 starId=-1235930289 frame=false + body 8396159_3733749_-457386 8396120_3733751_-457350 kind=PLANET orbit=284 radius=2.2417612943334566 starId=-1235930289 frame=true + body 8396159_3733749_-457386 8396128_3733749_-457307 kind=ASTEROID_BELT orbit=454 radius=0.0 starId=-1235930289 frame=true + body 8396159_3733749_-457386 8396147_3733750_-457385 kind=MOON orbit=64 radius=0.20798542465380418 starId=-1235930289 frame=false + body 8396159_3733749_-457386 8396147_3733750_-457385 kind=MOON orbit=64 radius=0.2733252772924753 starId=-1235930289 frame=false + body 8396159_3733749_-457386 8396147_3733750_-457385 kind=PLANET orbit=64 radius=0.8297832210543126 starId=-1235930289 frame=true + body 8396159_3733749_-457386 8396157_3733749_-457386 kind=PLANET orbit=9 radius=1.3026390694134573 starId=-1235930289 frame=true + body 8396159_3733749_-457386 8396159_3733749_-457386 kind=STAR orbit=0 radius=0.0 starId=-1235930289 frame=true + body 8396159_3733749_-457386 8396159_3733749_-457389 kind=MOON orbit=15 radius=0.2690640072563959 starId=-1235930289 frame=false + body 8396159_3733749_-457386 8396159_3733749_-457389 kind=PLANET orbit=15 radius=0.5873692699834249 starId=-1235930289 frame=true + body 8396159_3733749_-457386 8396165_3733749_-457390 kind=PLANET orbit=39 radius=0.6440401650263456 starId=-1235930289 frame=true + body 8396159_3733749_-457386 8396168_3733749_-457421 kind=PLANET orbit=195 radius=0.9249787785253212 starId=-1235930289 frame=true + body 874922_4033773_3027920 874922_4033773_3027920 kind=MOON orbit=0 radius=0.5867444102269814 starId=-1958754413 frame=false + body 874922_4033773_3027920 874922_4033773_3027920 kind=MOON orbit=0 radius=0.6081141830622963 starId=-1958754413 frame=false + body 874922_4033773_3027920 874922_4033773_3027920 kind=ROGUE_PLANET orbit=0 radius=2.2898780119292534 starId=-1958754413 frame=true + body 9101293_8238273_-4074920 9096316_8238459_-4076773 kind=MOON orbit=28418 radius=0.34028850086578666 starId=-1857245365 frame=false + body 9101293_8238273_-4074920 9096316_8238459_-4076773 kind=PLANET orbit=28418 radius=0.6877040920784976 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9101053_8238297_-4074078 kind=GAS_GIANT orbit=4682 radius=8.602364603527008 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9101075_8238268_-4074884 kind=ASTEROID_BELT orbit=1182 radius=0.0 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=GAS_GIANT orbit=2128 radius=8.688024092877786 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=MOON orbit=2128 radius=0.4558770711816329 starId=-1857245365 frame=false + body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=MOON orbit=2128 radius=0.517815056947268 starId=-1857245365 frame=false + body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=MOON orbit=2128 radius=0.5991392207919304 starId=-1857245365 frame=false + body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=MOON orbit=2128 radius=0.6197041873623639 starId=-1857245365 frame=false + body 9101293_8238273_-4074920 9101152_8238265_-4074753 kind=PLANET orbit=1171 radius=0.6675340300021155 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9101287_8238269_-4075013 kind=PLANET orbit=498 radius=1.949315024881134 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9101293_8238273_-4074920 kind=STAR orbit=0 radius=0.0 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9101309_8238211_-4076205 kind=PLANET orbit=6879 radius=2.055381652866782 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9101335_8238275_-4074965 kind=MOON orbit=330 radius=0.25776475988841896 starId=-1857245365 frame=false + body 9101293_8238273_-4074920 9101335_8238275_-4074965 kind=PLANET orbit=330 radius=0.8648616554787374 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9101349_8238185_-4077785 kind=GAS_GIANT orbit=15329 radius=6.6090226899882865 starId=-1857245365 frame=true + body 9101293_8238273_-4074920 9102690_8238598_-4083301 kind=ASTEROID_BELT orbit=45468 radius=0.0 starId=-1857245365 frame=true + body 9115079_5410474_1795046 9115079_5410474_1795046 kind=MOON orbit=0 radius=1.3206635975936905 starId=-1757034753 frame=false + body 9115079_5410474_1795046 9115079_5410474_1795046 kind=ROGUE_PLANET orbit=0 radius=1.0266176771058164 starId=-1757034753 frame=true + derived -147990_-4259982_-3782514 -147990_-4259982_-3782514 type=ice mass=0.004767026424118472 radius=0.2447237286529341 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=16562 metallicity=0.6374424070421512 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1763249_4241372_4386755 -1763249_4241372_4386755 type=ice mass=0.017224606058141433 radius=0.32767939488983133 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=44247 metallicity=0.47420974986188824 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1811609_-4167359_4728900 -1811609_-4167359_4728900 type=superearth mass=12.019652275506662 radius=1.8824394321228168 gravity=339 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=79080 metallicity=1.0470297959069352 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2114326_9425508_6200501 -2114326_9425508_6200501 type=ice mass=1.2997261978222874 radius=1.0893681258617856 gravity=110 pressure=0 tempK=36 oxygen=false locked=false rings=true rotation=27748 metallicity=1.2451647291371057 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2550052_6994691_-1786506 -2550052_6994691_-1786506 type=barren mass=0.01658361521488155 radius=0.32882272714498073 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=19446 metallicity=0.8523999939023407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2559146_-594813_8660842 -2559146_-594813_8660842 type=ice mass=2.443441381145703 radius=1.2175511349308201 gravity=165 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=58446 metallicity=0.5373070678961682 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3234634_7878403_530638 -3234634_7878403_530638 type=ice mass=2.9790234699827027 radius=1.3180441026495238 gravity=171 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=14746 metallicity=0.760972477157181 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4729070_1810660_-2040913 -4729070_1810660_-2040913 type=ice mass=2.859837455249325 radius=1.2653982081483837 gravity=179 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=79465 metallicity=0.3681566576342936 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -517007_1928357_7786828 -517007_1928357_7786828 type=ice mass=2.1103637839767204 radius=1.2632316197428803 gravity=132 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=14112 metallicity=1.1055450947112693 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446221_9823217_1140222 type=icegiant mass=316.4815577035859 radius=10.977132265159819 gravity=263 pressure=1600 tempK=121 oxygen=false locked=false rings=true rotation=8336 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446277_9823217_1140255 type=gasgiant mass=132.85204474218668 radius=7.526351165166979 gravity=235 pressure=1600 tempK=177 oxygen=false locked=false rings=true rotation=13373 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446282_9823217_1140274 type=exotic mass=2.8042763914170217 radius=1.3635217085174838 gravity=151 pressure=1600 tempK=301 oxygen=false locked=true rings=false rotation=11035 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446285_9823217_1140269 type=greenhouse mass=2.9736945063864484 radius=1.3471617398696347 gravity=164 pressure=1600 tempK=293 oxygen=false locked=true rings=false rotation=18483 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446287_9823217_1140272 type=exotic mass=1.7747236936705673 radius=1.1015474620849677 gravity=146 pressure=125 tempK=361 oxygen=false locked=true rings=false rotation=43408 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446288_9823217_1140272 type=lava mass=22.49954928588334 radius=2.4811969807180527 gravity=365 pressure=1600 tempK=1925 oxygen=false locked=true rings=false rotation=58095 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446289_9823217_1140273 type=barren mass=0.0031867611215720924 radius=0.2092017435866524 gravity=7 pressure=0 tempK=269 oxygen=false locked=true rings=false rotation=9742 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446289_9823217_1140275 type=barren mass=0.0033234774498898385 radius=0.2114444291012643 gravity=7 pressure=0 tempK=201 oxygen=false locked=true rings=false rotation=9600 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446290_9823217_1140262 type=ice mass=0.0031894721532622593 radius=0.2281630648172823 gravity=6 pressure=0 tempK=95 oxygen=false locked=false rings=false rotation=8365 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446292_9823217_1140264 type=exotic mass=6.084905877266164 radius=1.6741507494893029 gravity=217 pressure=1600 tempK=265 oxygen=false locked=false rings=false rotation=7987 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446298_9823215_1140303 type=ice mass=0.06887354574092128 radius=0.4706609388538775 gravity=31 pressure=46 tempK=65 oxygen=false locked=false rings=false rotation=12192 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2446288_9823217_1140272 2446305_9823217_1140274 type=gasgiant mass=83.58487296657829 radius=6.153023641353679 gravity=221 pressure=1600 tempK=190 oxygen=false locked=false rings=false rotation=8617 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 331018_-2627546_4002550 331018_-2627546_4002550 type=ice mass=0.19202465319892836 radius=0.648198320022664 gravity=46 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=9798 metallicity=0.4262158195671292 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3810943_7578176_-1529346 3810832_7578170_-1529377 type=ice mass=0.0032286372068856587 radius=0.21061448925959111 gravity=7 pressure=0 tempK=78 oxygen=false locked=false rings=false rotation=10609 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3810943_7578176_-1529346 3810930_7578178_-1529283 type=exotic mass=1.5221744884553101 radius=1.1579452921819666 gravity=114 pressure=1600 tempK=270 oxygen=false locked=false rings=false rotation=6121 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3810943_7578176_-1529346 3810940_7578176_-1529345 type=barren mass=0.0029832573614254915 radius=0.2158682404627622 gravity=6 pressure=0 tempK=534 oxygen=false locked=true rings=false rotation=11396 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3810943_7578176_-1529346 3810943_7578176_-1529346 type=barren mass=0.040851204012771695 radius=0.42132762762736997 gravity=23 pressure=0 tempK=982 oxygen=false locked=true rings=false rotation=41772 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3810943_7578176_-1529346 3810948_7578177_-1529360 type=superearth mass=16.848537562523344 radius=2.1277124765728215 gravity=372 pressure=1600 tempK=557 oxygen=false locked=false rings=false rotation=26542 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3810943_7578176_-1529346 3810969_7578175_-1529352 type=barren mass=0.04553182766757045 radius=0.4201376811063558 gravity=26 pressure=2 tempK=195 oxygen=false locked=false rings=false rotation=69585 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3810943_7578176_-1529346 3811028_7578212_-1530104 type=ice mass=1.091203688879023 radius=0.9816292025981661 gravity=113 pressure=1600 tempK=68 oxygen=false locked=false rings=false rotation=84653 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3810943_7578176_-1529346 3811141_7578179_-1529254 type=ice mass=0.03591972812186142 radius=0.3932058876865541 gravity=23 pressure=11 tempK=56 oxygen=false locked=false rings=false rotation=7128 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3810943_7578176_-1529346 3811237_7578161_-1529722 type=ice mass=6.47400061340064 radius=1.6686795831939623 gravity=233 pressure=1600 tempK=86 oxygen=false locked=false rings=false rotation=44932 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4285126_880860_5633403 4285059_880873_5632810 type=icegiant mass=24.78871662757518 radius=3.6272568021231066 gravity=188 pressure=1600 tempK=75 oxygen=false locked=false rings=true rotation=7544 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4285126_880860_5633403 4285073_880861_5633289 type=ice mass=1.6015171943747801 radius=1.128421875313703 gravity=126 pressure=1600 tempK=155 oxygen=false locked=false rings=false rotation=61867 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4285126_880860_5633403 4285102_880861_5633396 type=barren mass=0.15881044989458584 radius=0.5918769282708047 gravity=45 pressure=21 tempK=188 oxygen=false locked=false rings=false rotation=31692 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4285126_880860_5633403 4285109_880858_5633362 type=gasgiant mass=28.512121925562884 radius=3.854805170021379 gravity=192 pressure=1600 tempK=274 oxygen=false locked=false rings=true rotation=13921 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4285126_880860_5633403 4285126_880860_5633403 type=lava mass=19.30475444324071 radius=2.131253834451699 gravity=400 pressure=485 tempK=3086 oxygen=false locked=true rings=false rotation=20687 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4285126_880860_5633403 4285126_880860_5633405 type=lava mass=18.538429815154963 radius=2.1119381062001854 gravity=400 pressure=1600 tempK=1529 oxygen=false locked=true rings=false rotation=21622 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4285126_880860_5633403 4285131_880860_5633401 type=lava mass=17.701478105223508 radius=2.0812185825857674 gravity=400 pressure=1600 tempK=891 oxygen=false locked=true rings=false rotation=79691 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4285126_880860_5633403 4285136_880859_5633399 type=superearth mass=3.890518639874639 radius=1.401970044249525 gravity=198 pressure=735 tempK=503 oxygen=false locked=false rings=false rotation=66507 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4285126_880860_5633403 4285497_880855_5633365 type=barren mass=0.046542099334781754 radius=0.4319970136446857 gravity=25 pressure=6 tempK=48 oxygen=false locked=false rings=false rotation=48150 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4287194_-1179392_-2651040 4287194_-1179392_-2651040 type=barren mass=0.10833361309522743 radius=0.5675357443169351 gravity=34 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=43418 metallicity=1.4362747689344162 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4540244_-2195284_6241886 4540243_-2195284_6241883 type=barren mass=0.02034697498380806 radius=0.35933183620673187 gravity=16 pressure=0 tempK=473 oxygen=false locked=true rings=false rotation=14054 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4540244_-2195284_6241886 4540244_-2195284_6241886 type=lava mass=0.11540333451517869 radius=0.5678878284656146 gravity=36 pressure=0 tempK=1842 oxygen=false locked=true rings=false rotation=34812 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4540244_-2195284_6241886 4540250_-2195286_6241917 type=barren mass=0.079846587952403 radius=0.525683512535432 gravity=29 pressure=9 tempK=141 oxygen=false locked=false rings=false rotation=25667 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4540244_-2195284_6241886 4540258_-2195290_6241733 type=ice mass=0.05263347777872866 radius=0.44478714057429125 gravity=27 pressure=23 tempK=52 oxygen=false locked=false rings=false rotation=53953 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4540244_-2195284_6241886 4540402_-2195292_6242073 type=ice mass=1.137232749072275 radius=1.0699392330221018 gravity=99 pressure=1600 tempK=93 oxygen=false locked=false rings=false rotation=20557 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 548615_3409698_-890024 548615_3409698_-890024 type=ice mass=0.8316322455580678 radius=0.9755138376471226 gravity=87 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=51282 metallicity=0.41829481403499663 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5659038_1332946_1424772 5658972_1332942_1424881 type=gasgiant mass=153.02064266490837 radius=8.003357331978748 gravity=239 pressure=1600 tempK=144 oxygen=false locked=false rings=true rotation=7155 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5659038_1332946_1424772 5659002_1332946_1424807 type=superearth mass=6.594081074411881 radius=1.680261648083262 gravity=234 pressure=1600 tempK=249 oxygen=false locked=false rings=false rotation=13964 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5659038_1332946_1424772 5659031_1332945_1424786 type=greenhouse mass=19.690248030506307 radius=2.3821474285420066 gravity=347 pressure=1600 tempK=339 oxygen=false locked=false rings=false rotation=6424 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5659038_1332946_1424772 5659038_1332946_1424767 type=desert mass=0.15059433565894353 radius=0.5652156493519473 gravity=47 pressure=5 tempK=337 oxygen=false locked=true rings=false rotation=10188 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5659038_1332946_1424772 5659038_1332946_1424772 type=lava mass=3.3653614720096416 radius=1.3993165606585976 gravity=172 pressure=16 tempK=1901 oxygen=false locked=true rings=false rotation=29675 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5659038_1332946_1424772 5659146_1332945_1424974 type=gasgiant mass=271.4384670839881 radius=10.26830810198086 gravity=257 pressure=1600 tempK=108 oxygen=false locked=false rings=true rotation=8668 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5659038_1332946_1424772 5659218_1332958_1425091 type=ice mass=2.047358782492318 radius=1.2985499750340264 gravity=121 pressure=1600 tempK=80 oxygen=false locked=false rings=false rotation=6419 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6253955_-322304_1235104 6253955_-322304_1235104 type=barren mass=0.044689233799970744 radius=0.4073729049004505 gravity=27 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=35413 metallicity=0.7485853832713572 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6326499_-4297175_-3049933 6326499_-4297175_-3049933 type=barren mass=0.0051146307901779745 radius=0.23300568312102693 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=16827 metallicity=0.5074826406907766 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7518342_4618068_5122633 7518342_4618068_5122633 type=ice mass=0.021854390659360623 radius=0.3709252755727711 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=33784 metallicity=0.6229085872163995 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 783965_8560023_8900358 783965_8560023_8900358 type=ice mass=2.0737223615710203 radius=1.2840791507608935 gravity=126 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=76518 metallicity=1.5884364730460865 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8253893_-3146511_6379169 8253883_-3146511_6384549 type=ice mass=0.2344219835904629 radius=0.687466425152063 gravity=50 pressure=466 tempK=7 oxygen=false locked=false rings=false rotation=37472 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8253893_-3146511_6379169 8253893_-3146511_6379169 type=lava mass=19.104993593296804 radius=2.3010202631136 gravity=361 pressure=572 tempK=1559 oxygen=false locked=true rings=false rotation=10571 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8253893_-3146511_6379169 8253895_-3146511_6379168 type=barren mass=0.025566217269380617 radius=0.35018276058976605 gravity=21 pressure=0 tempK=247 oxygen=false locked=true rings=false rotation=44160 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8253893_-3146511_6379169 8253899_-3146511_6379172 type=superearth mass=12.683388525114609 radius=1.9849839958996087 gravity=322 pressure=1600 tempK=307 oxygen=false locked=true rings=false rotation=10455 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8253893_-3146511_6379169 8253909_-3146510_6379156 type=barren mass=0.011087535052431922 radius=0.28522355165984403 gravity=14 pressure=1 tempK=84 oxygen=false locked=false rings=false rotation=19148 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8253893_-3146511_6379169 8253942_-3146510_6379142 type=superearth mass=21.05654269918682 radius=2.1533492603769537 gravity=400 pressure=1600 tempK=109 oxygen=false locked=false rings=false rotation=14630 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8253893_-3146511_6379169 8253955_-3146511_6379105 type=ice mass=1.012653887217379 radius=0.9682541436385077 gravity=108 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=13322 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8259566_6483340_6516020 8259566_6483340_6516020 type=barren mass=0.012722502496168983 radius=0.29300354320951977 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=14151 metallicity=1.5687224409653222 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8396159_3733749_-457386 8396120_3733751_-457350 type=superearth mass=23.286398057466076 radius=2.2417612943334566 gravity=400 pressure=1600 tempK=117 oxygen=false locked=false rings=false rotation=7102 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8396159_3733749_-457386 8396128_3733749_-457307 type=superearth mass=9.812214870978613 radius=1.8690561684738647 gravity=281 pressure=1600 tempK=93 oxygen=false locked=false rings=false rotation=11719 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8396159_3733749_-457386 8396147_3733750_-457385 type=ice mass=0.5643326340921984 radius=0.8297832210543126 gravity=82 pressure=830 tempK=183 oxygen=false locked=false rings=false rotation=8552 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8396159_3733749_-457386 8396157_3733749_-457386 type=greenhouse mass=2.679147391530122 radius=1.3026390694134573 gravity=158 pressure=734 tempK=421 oxygen=false locked=true rings=false rotation=13905 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8396159_3733749_-457386 8396159_3733749_-457386 type=barren mass=0.17280756683991655 radius=0.6024715626687684 gravity=48 pressure=0 tempK=935 oxygen=false locked=true rings=false rotation=53296 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8396159_3733749_-457386 8396159_3733749_-457389 type=barren mass=0.15638614311529456 radius=0.5873692699834249 gravity=45 pressure=2 tempK=241 oxygen=false locked=true rings=false rotation=54488 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8396159_3733749_-457386 8396165_3733749_-457390 type=ice mass=0.16244683723102227 radius=0.6440401650263456 gravity=39 pressure=26 tempK=122 oxygen=false locked=true rings=false rotation=14579 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8396159_3733749_-457386 8396168_3733749_-457421 type=ice mass=0.8468118224661318 radius=0.9249787785253212 gravity=99 pressure=1600 tempK=123 oxygen=false locked=false rings=false rotation=33385 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 874922_4033773_3027920 874922_4033773_3027920 type=superearth mass=25.378726470010758 radius=2.2898780119292534 gravity=400 pressure=0 tempK=52 oxygen=false locked=false rings=false rotation=20865 metallicity=0.8292978022576905 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9096316_8238459_-4076773 type=ice mass=0.2572052366743317 radius=0.6877040920784976 gravity=54 pressure=968 tempK=81 oxygen=false locked=false rings=false rotation=26980 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9101053_8238297_-4074078 type=icegiant mass=180.65292154779496 radius=8.602364603527008 gravity=244 pressure=1600 tempK=239 oxygen=false locked=false rings=true rotation=6403 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9101075_8238268_-4074884 type=greenhouse mass=20.20506361182462 radius=2.26358459386985 gravity=394 pressure=1600 tempK=401 oxygen=false locked=false rings=false rotation=7816 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9101095_8238254_-4074575 type=gasgiant mass=184.81715571505165 radius=8.688024092877786 gravity=245 pressure=1600 tempK=355 oxygen=false locked=false rings=false rotation=5906 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9101152_8238265_-4074753 type=barren mass=0.21047510072471026 radius=0.6675340300021155 gravity=47 pressure=19 tempK=245 oxygen=false locked=false rings=false rotation=11999 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9101287_8238269_-4075013 type=superearth mass=10.816647345624249 radius=1.949315024881134 gravity=285 pressure=1600 tempK=800 oxygen=false locked=false rings=false rotation=90594 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9101293_8238273_-4074920 type=unclassified mass=6.3839884707604115 radius=1.7569735099238113 gravity=207 pressure=1 tempK=7936 oxygen=false locked=true rings=false rotation=16466 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9101309_8238211_-4076205 type=superearth mass=13.734120478770192 radius=2.055381652866782 gravity=325 pressure=1600 tempK=215 oxygen=false locked=false rings=false rotation=17909 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9101335_8238275_-4074965 type=desert mass=0.6863398424781832 radius=0.8648616554787374 gravity=92 pressure=34 tempK=436 oxygen=false locked=false rings=false rotation=16923 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9101349_8238185_-4077785 type=icegiant mass=98.5234480978337 radius=6.6090226899882865 gravity=226 pressure=1600 tempK=132 oxygen=false locked=false rings=true rotation=8317 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9101293_8238273_-4074920 9102690_8238598_-4083301 type=gasgiant mass=63.509722984799666 radius=5.460400114589725 gravity=213 pressure=1600 tempK=76 oxygen=false locked=false rings=false rotation=5180 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9115079_5410474_1795046 9115079_5410474_1795046 type=ice mass=1.3086926327482205 radius=1.0266176771058164 gravity=124 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=8011 metallicity=1.5865813077558517 terrain=TerrainOption[NATIVE genType=0 w=1] + system -147990_-4259982_-3782514 id=-1021692653 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless + system -1763249_4241372_4386755 id=-1507112769 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless + system -1811609_-4167359_4728900 id=-1319680873 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless + system -2114326_9425508_6200501 id=-525238425 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless + system -2550052_6994691_-1786506 id=-1940610717 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless + system -2559146_-594813_8660842 id=-322493161 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless + system -3234634_7878403_530638 id=-782417921 kind=ROGUE_PLANET name=PGR--5002361.5002361.0 starless + system -4729070_1810660_-2040913 id=-1026598645 kind=ROGUE_PLANET name=PGR--5002361.0.-5002361 starless + system -517007_1928357_7786828 id=-819120817 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless + system 2446288_9823217_1140272 id=-677694141 kind=STAR name=PGS-0.5002361.0 starTemp=40 starSize=0.6166995763778687 + system 331018_-2627546_4002550 id=-778284213 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless + system 3810943_7578176_-1529346 id=-1781882117 kind=STAR name=PGS-0.5002361.-5002361 starTemp=40 starSize=0.7512625455856323 + system 4285126_880860_5633403 id=-1076361445 kind=STAR name=PGS-0.0.5002361 starTemp=70 starSize=0.9343165159225464 + system 4287194_-1179392_-2651040 id=-493026021 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless + system 4540244_-2195284_6241886 id=-1311232065 kind=STAR name=PGS-0.-5002361.5002361 starTemp=70 starSize=0.9332678318023682 + system 548615_3409698_-890024 id=-502144617 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless + system 5659038_1332946_1424772 id=-1094984105 kind=STAR name=PGS-5002361.0.0 starTemp=70 starSize=0.9943618178367615 + system 6253955_-322304_1235104 id=-1379053845 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless + system 6326499_-4297175_-3049933 id=-335655149 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless + system 7518342_4618068_5122633 id=-1527090629 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless + system 783965_8560023_8900358 id=-602652429 kind=ROGUE_PLANET name=PGR-0.5002361.5002361 starless + system 8253893_-3146511_6379169 id=-1913306833 kind=STAR name=PGS-5002361.-5002361.5002361 starTemp=40 starSize=0.6768931150436401 + system 8259566_6483340_6516020 id=-373298525 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless + system 8396159_3733749_-457386 id=-1235930289 kind=STAR name=PGS-5002361.0.-5002361 starTemp=40 starSize=0.7446791529655457 + system 874922_4033773_3027920 id=-1958754413 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 9101293_8238273_-4074920 id=-1857245365 kind=STAR name=PGS-5002361.5002361.-5002361 starTemp=220 starSize=1.9884947538375854 + system 9115079_5410474_1795046 id=-1757034753 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless +seed 1337 systems=27 + body -1573709_2153652_-2170228 -1573709_2153652_-2170228 kind=ROGUE_PLANET orbit=0 radius=0.7689629830810678 starId=-1458129621 frame=true + body -2063555_7189287_7920294 -2063555_7189287_7920294 kind=MOON orbit=0 radius=1.9211387591656388 starId=-225880809 frame=false + body -2063555_7189287_7920294 -2063555_7189287_7920294 kind=ROGUE_PLANET orbit=0 radius=0.46316320420630236 starId=-225880809 frame=true + body -2140810_2039107_9279683 -2140810_2039107_9279683 kind=ROGUE_PLANET orbit=0 radius=0.2085263508076044 starId=-243099345 frame=true + body -3336357_4475214_3033351 -3336357_4475214_3033351 kind=MOON orbit=0 radius=1.2087626383751437 starId=-629791769 frame=false + body -3336357_4475214_3033351 -3336357_4475214_3033351 kind=MOON orbit=0 radius=1.5941117666200255 starId=-629791769 frame=false + body -3336357_4475214_3033351 -3336357_4475214_3033351 kind=ROGUE_PLANET orbit=0 radius=0.6229314360348599 starId=-629791769 frame=true + body -364554_6854024_-3395136 -364554_6854024_-3395136 kind=MOON orbit=0 radius=0.3810385920443371 starId=-180255469 frame=false + body -364554_6854024_-3395136 -364554_6854024_-3395136 kind=ROGUE_PLANET orbit=0 radius=2.252290185189226 starId=-180255469 frame=true + body -3941235_-1709038_3545575 -3941235_-1709038_3545575 kind=MOON orbit=0 radius=0.6196082002556256 starId=-1370886497 frame=false + body -3941235_-1709038_3545575 -3941235_-1709038_3545575 kind=ROGUE_PLANET orbit=0 radius=1.1985513611208807 starId=-1370886497 frame=true + body -4666123_-580567_5682104 -4666123_-580567_5682104 kind=MOON orbit=0 radius=2.1715494207305213 starId=-1303466537 frame=false + body -4666123_-580567_5682104 -4666123_-580567_5682104 kind=MOON orbit=0 radius=2.3151726622408 starId=-1303466537 frame=false + body -4666123_-580567_5682104 -4666123_-580567_5682104 kind=ROGUE_PLANET orbit=0 radius=0.40772533876055145 starId=-1303466537 frame=true + body -554372_-4442061_-774973 -554372_-4442061_-774973 kind=MOON orbit=0 radius=0.4396589713261482 starId=-291544557 frame=false + body -554372_-4442061_-774973 -554372_-4442061_-774973 kind=MOON orbit=0 radius=1.1299659557046622 starId=-291544557 frame=false + body -554372_-4442061_-774973 -554372_-4442061_-774973 kind=ROGUE_PLANET orbit=0 radius=2.0544146928745537 starId=-291544557 frame=true + body -626941_5154507_4503681 -626941_5154507_4503681 kind=MOON orbit=0 radius=0.9887336142199961 starId=-789817773 frame=false + body -626941_5154507_4503681 -626941_5154507_4503681 kind=ROGUE_PLANET orbit=0 radius=1.687645291523904 starId=-789817773 frame=true + body 1228651_-2557618_8861204 1228651_-2557618_8861204 kind=MOON orbit=0 radius=0.6899148440246614 starId=-548985509 frame=false + body 1228651_-2557618_8861204 1228651_-2557618_8861204 kind=MOON orbit=0 radius=0.8075854491022794 starId=-548985509 frame=false + body 1228651_-2557618_8861204 1228651_-2557618_8861204 kind=ROGUE_PLANET orbit=0 radius=2.0824746625433077 starId=-548985509 frame=true + body 2274979_7150205_-1755432 2274979_7150205_-1755432 kind=ROGUE_PLANET orbit=0 radius=0.6413676617295493 starId=-840467989 frame=true + body 2691671_5432625_7859903 2691587_5432625_7859996 kind=STAR orbit=670 radius=91.48043859779835 starId=-1879670171 frame=true + body 2691671_5432625_7859903 2691671_5432625_7859903 kind=STAR orbit=0 radius=0.0 starId=-1879670169 frame=true + body 2691671_5432625_7859903 2691672_5432625_7859909 kind=STAR orbit=32 radius=91.56488044381142 starId=-1879670170 frame=true + body 2691671_5432625_7859903 2691672_5432625_7859923 kind=PLANET orbit=106 radius=1.073167880211255 starId=-1879670169 frame=true + body 2691671_5432625_7859903 2691701_5432626_7859911 kind=ASTEROID_BELT orbit=169 radius=0.0 starId=-1879670169 frame=true + body 3486529_6795101_617493 3486529_6795101_617493 kind=ROGUE_PLANET orbit=0 radius=2.367000742190591 starId=-1428201685 frame=true + body 3662998_191651_-1822417 3662998_191651_-1822417 kind=MOON orbit=0 radius=1.147549354034543 starId=-41240637 frame=false + body 3662998_191651_-1822417 3662998_191651_-1822417 kind=ROGUE_PLANET orbit=0 radius=2.3848465086399933 starId=-41240637 frame=true + body 3744070_1703327_2446882 3744070_1703327_2446882 kind=ROGUE_PLANET orbit=0 radius=1.1019636237102468 starId=-342178325 frame=true + body 4347096_3736058_9064309 4347096_3736058_9064309 kind=ROGUE_PLANET orbit=0 radius=0.3183726327938388 starId=-439396409 frame=true + body 528043_-2545788_1919161 528043_-2545788_1919161 kind=MOON orbit=0 radius=2.31808440045958 starId=-1278118149 frame=false + body 528043_-2545788_1919161 528043_-2545788_1919161 kind=MOON orbit=0 radius=2.3864950791588417 starId=-1278118149 frame=false + body 528043_-2545788_1919161 528043_-2545788_1919161 kind=ROGUE_PLANET orbit=0 radius=0.7363821812286615 starId=-1278118149 frame=true + body 5948611_6639061_7642905 5948611_6639061_7642905 kind=MOON orbit=0 radius=0.2899400912366618 starId=-775167053 frame=false + body 5948611_6639061_7642905 5948611_6639061_7642905 kind=MOON orbit=0 radius=0.35359605598253835 starId=-775167053 frame=false + body 5948611_6639061_7642905 5948611_6639061_7642905 kind=ROGUE_PLANET orbit=0 radius=1.8870973054918487 starId=-775167053 frame=true + body 6681053_-1258075_6889516 6681053_-1258075_6889516 kind=MOON orbit=0 radius=0.2135741271197545 starId=-557819189 frame=false + body 6681053_-1258075_6889516 6681053_-1258075_6889516 kind=MOON orbit=0 radius=1.597241773129025 starId=-557819189 frame=false + body 6681053_-1258075_6889516 6681053_-1258075_6889516 kind=ROGUE_PLANET orbit=0 radius=0.3754924381667063 starId=-557819189 frame=true + body 6822102_5146281_2692793 6822102_5146281_2692793 kind=MOON orbit=0 radius=0.2558111354762624 starId=-652918041 frame=false + body 6822102_5146281_2692793 6822102_5146281_2692793 kind=ROGUE_PLANET orbit=0 radius=0.24605547826027752 starId=-652918041 frame=true + body 7474804_-1243737_1670574 7474804_-1243737_1670574 kind=MOON orbit=0 radius=0.9621043876085937 starId=-176624233 frame=false + body 7474804_-1243737_1670574 7474804_-1243737_1670574 kind=MOON orbit=0 radius=1.5767149252404509 starId=-176624233 frame=false + body 7474804_-1243737_1670574 7474804_-1243737_1670574 kind=ROGUE_PLANET orbit=0 radius=1.0177477139653435 starId=-176624233 frame=true + body 756689_-1996515_-853506 756689_-1996515_-853506 kind=MOON orbit=0 radius=1.5652194226482485 starId=-1395994849 frame=false + body 756689_-1996515_-853506 756689_-1996515_-853506 kind=MOON orbit=0 radius=2.4105363035989726 starId=-1395994849 frame=false + body 756689_-1996515_-853506 756689_-1996515_-853506 kind=ROGUE_PLANET orbit=0 radius=0.32387786813174946 starId=-1395994849 frame=true + body 7769045_2585852_903758 7769002_2585853_903885 kind=ASTEROID_BELT orbit=718 radius=0.0 starId=-1675040469 frame=true + body 7769045_2585852_903758 7769003_2585851_903765 kind=PLANET orbit=227 radius=0.48703923768915025 starId=-1675040469 frame=true + body 7769045_2585852_903758 7769033_2585852_903752 kind=PLANET orbit=70 radius=2.4851009831751356 starId=-1675040469 frame=true + body 7769045_2585852_903758 7769038_2585852_903761 kind=PLANET orbit=41 radius=0.7871550612532745 starId=-1675040469 frame=true + body 7769045_2585852_903758 7769044_2585852_903763 kind=PLANET orbit=27 radius=0.382747324712566 starId=-1675040469 frame=true + body 7769045_2585852_903758 7769045_2585852_903757 kind=PLANET orbit=8 radius=1.0470728760292016 starId=-1675040469 frame=true + body 7769045_2585852_903758 7769045_2585852_903758 kind=STAR orbit=0 radius=0.0 starId=-1675040469 frame=true + body 7769045_2585852_903758 7769047_2585852_903756 kind=PLANET orbit=15 radius=1.3656342123398215 starId=-1675040469 frame=true + body 7769045_2585852_903758 7769065_2585851_903737 kind=MOON orbit=155 radius=0.21152405129733184 starId=-1675040469 frame=false + body 7769045_2585852_903758 7769065_2585851_903737 kind=PLANET orbit=155 radius=0.27688238134245285 starId=-1675040469 frame=true + body 7769045_2585852_903758 7769090_2585855_903687 kind=PLANET orbit=449 radius=0.4262581479440023 starId=-1675040469 frame=true + body 8273292_573611_-2696544 8273292_573611_-2696544 kind=MOON orbit=0 radius=0.288726105158068 starId=-1739755397 frame=false + body 8273292_573611_-2696544 8273292_573611_-2696544 kind=ROGUE_PLANET orbit=0 radius=0.43887128385988716 starId=-1739755397 frame=true + body 8446414_4393296_7956124 8446414_4393296_7956124 kind=ROGUE_PLANET orbit=0 radius=2.191233610427466 starId=-264084353 frame=true + body 8839150_8100443_-3684508 8839150_8100443_-3684508 kind=MOON orbit=0 radius=0.40758051197899203 starId=-91668937 frame=false + body 8839150_8100443_-3684508 8839150_8100443_-3684508 kind=MOON orbit=0 radius=1.0607923338311969 starId=-91668937 frame=false + body 8839150_8100443_-3684508 8839150_8100443_-3684508 kind=ROGUE_PLANET orbit=0 radius=0.31270392841251154 starId=-91668937 frame=true + body 8988904_-910550_-4036263 8988904_-910550_-4036263 kind=MOON orbit=0 radius=1.0074973919978136 starId=-1207384369 frame=false + body 8988904_-910550_-4036263 8988904_-910550_-4036263 kind=ROGUE_PLANET orbit=0 radius=1.1458517936303287 starId=-1207384369 frame=true + derived -1573709_2153652_-2170228 -1573709_2153652_-2170228 type=barren mass=0.32414223439268397 radius=0.7689629830810678 gravity=55 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=7632 metallicity=1.4554282434374612 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2063555_7189287_7920294 -2063555_7189287_7920294 type=ice mass=0.06051953130553028 radius=0.46316320420630236 gravity=28 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=37697 metallicity=0.6844507656397616 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2140810_2039107_9279683 -2140810_2039107_9279683 type=barren mass=0.002751983861320743 radius=0.2085263508076044 gravity=6 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=32283 metallicity=1.2079601860651414 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3336357_4475214_3033351 -3336357_4475214_3033351 type=ice mass=0.16019483571353615 radius=0.6229314360348599 gravity=41 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=11669 metallicity=0.5158513859142492 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -364554_6854024_-3395136 -364554_6854024_-3395136 type=ice mass=20.83809474201407 radius=2.252290185189226 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=8566 metallicity=0.8424423233343017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3941235_-1709038_3545575 -3941235_-1709038_3545575 type=ice mass=2.233324231141556 radius=1.1985513611208807 gravity=155 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=21930 metallicity=1.4897595589858756 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4666123_-580567_5682104 -4666123_-580567_5682104 type=barren mass=0.03317275208369183 radius=0.40772533876055145 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=41584 metallicity=1.0115030110151066 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -554372_-4442061_-774973 -554372_-4442061_-774973 type=ice mass=17.253752883812503 radius=2.0544146928745537 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=9670 metallicity=0.9905132772116106 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -626941_5154507_4503681 -626941_5154507_4503681 type=ice mass=8.39157323910817 radius=1.687645291523904 gravity=295 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=34181 metallicity=1.0878429053553482 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1228651_-2557618_8861204 1228651_-2557618_8861204 type=ice mass=14.548057887563953 radius=2.0824746625433077 gravity=335 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=6353 metallicity=1.365480786121032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2274979_7150205_-1755432 2274979_7150205_-1755432 type=ice mass=0.1500505517262651 radius=0.6413676617295493 gravity=36 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=13849 metallicity=1.2204472319378885 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2691671_5432625_7859903 2691587_5432625_7859996 type=barren mass=0.026654851089978983 radius=0.36753897512388545 gravity=20 pressure=8 tempK=48 oxygen=false locked=false rings=false rotation=11652 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2691671_5432625_7859903 2691671_5432625_7859903 type=lava mass=21.046812853607555 radius=2.4009953470502263 gravity=365 pressure=444 tempK=1630 oxygen=false locked=true rings=false rotation=16494 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2691671_5432625_7859903 2691672_5432625_7859909 type=exotic mass=0.9593281688075637 radius=0.9777208710388154 gravity=100 pressure=633 tempK=327 oxygen=false locked=true rings=false rotation=51981 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2691671_5432625_7859903 2691672_5432625_7859923 type=exotic mass=1.329896361560754 radius=1.073167880211255 gravity=115 pressure=1600 tempK=241 oxygen=false locked=false rings=false rotation=57635 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2691671_5432625_7859903 2691701_5432626_7859911 type=gasgiant mass=259.3928856424584 radius=10.067644615631313 gravity=256 pressure=1600 tempK=177 oxygen=false locked=false rings=true rotation=9821 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3486529_6795101_617493 3486529_6795101_617493 type=superearth mass=28.824756109067664 radius=2.367000742190591 gravity=400 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=61701 metallicity=0.851184826279871 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3662998_191651_-1822417 3662998_191651_-1822417 type=ice mass=29.702940798839204 radius=2.3848465086399933 gravity=400 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=9412 metallicity=1.0900309737699718 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3744070_1703327_2446882 3744070_1703327_2446882 type=ice mass=1.2538592333737375 radius=1.1019636237102468 gravity=103 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=33387 metallicity=1.0770181111183947 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4347096_3736058_9064309 4347096_3736058_9064309 type=barren mass=0.01194221705201743 radius=0.3183726327938388 gravity=12 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=42431 metallicity=0.8600542573611804 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 528043_-2545788_1919161 528043_-2545788_1919161 type=ice mass=0.26662901782290777 radius=0.7363821812286615 gravity=49 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=90972 metallicity=1.0255548888407957 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5948611_6639061_7642905 5948611_6639061_7642905 type=superearth mass=9.150169015332779 radius=1.8870973054918487 gravity=257 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=10868 metallicity=0.3794022609927985 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6681053_-1258075_6889516 6681053_-1258075_6889516 type=barren mass=0.028288794565751216 radius=0.3754924381667063 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=33130 metallicity=0.4759672475361737 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6822102_5146281_2692793 6822102_5146281_2692793 type=barren mass=0.004813129534930833 radius=0.24605547826027752 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=6712 metallicity=0.6084353447517761 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7474804_-1243737_1670574 7474804_-1243737_1670574 type=ice mass=1.259889576391659 radius=1.0177477139653435 gravity=122 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=79353 metallicity=0.7933269028516394 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 756689_-1996515_-853506 756689_-1996515_-853506 type=ice mass=0.016648914723364397 radius=0.32387786813174946 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=15800 metallicity=1.23751877539955 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769002_2585853_903885 type=gasgiant mass=91.5463181268455 radius=6.401300779274569 gravity=223 pressure=1600 tempK=74 oxygen=false locked=false rings=true rotation=8210 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769003_2585851_903765 type=ice mass=0.057333353032806525 radius=0.48703923768915025 gravity=24 pressure=11 tempK=55 oxygen=false locked=false rings=false rotation=8333 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769033_2585852_903752 type=superearth mass=32.93467878722113 radius=2.4851009831751356 gravity=400 pressure=1600 tempK=259 oxygen=false locked=false rings=false rotation=49878 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769038_2585852_903761 type=ice mass=0.3697306078750307 radius=0.7871550612532745 gravity=60 pressure=83 tempK=140 oxygen=false locked=true rings=false rotation=15596 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769044_2585852_903763 type=barren mass=0.03096611566502426 radius=0.382747324712566 gravity=21 pressure=1 tempK=196 oxygen=false locked=true rings=false rotation=41416 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769045_2585852_903757 type=greenhouse mass=1.4046047539899567 radius=1.0470728760292016 gravity=128 pressure=247 tempK=371 oxygen=false locked=true rings=false rotation=17124 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769045_2585852_903758 type=lava mass=1.5088556417880772 radius=1.0678552850270082 gravity=132 pressure=19 tempK=1026 oxygen=false locked=true rings=false rotation=16998 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769047_2585852_903756 type=greenhouse mass=2.7852326393056224 radius=1.3656342123398215 gravity=149 pressure=1600 tempK=433 oxygen=false locked=true rings=false rotation=43075 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769065_2585851_903737 type=ice mass=0.0082693671900693 radius=0.27688238134245285 gravity=11 pressure=1 tempK=67 oxygen=false locked=false rings=false rotation=11817 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7769045_2585852_903758 7769090_2585855_903687 type=barren mass=0.03545481939276029 radius=0.4262581479440023 gravity=20 pressure=7 tempK=48 oxygen=false locked=false rings=false rotation=67577 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8273292_573611_-2696544 8273292_573611_-2696544 type=ice mass=0.044845367952230576 radius=0.43887128385988716 gravity=23 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=18491 metallicity=0.42756566034063687 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8446414_4393296_7956124 8446414_4393296_7956124 type=superearth mass=20.256977086630656 radius=2.191233610427466 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=14500 metallicity=1.0258335666185951 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8839150_8100443_-3684508 8839150_8100443_-3684508 type=barren mass=0.015379281393746926 radius=0.31270392841251154 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=15514 metallicity=1.2063989283574807 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8988904_-910550_-4036263 8988904_-910550_-4036263 type=ice mass=1.9383555642911707 radius=1.1458517936303287 gravity=148 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=46415 metallicity=1.3865836729416734 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1573709_2153652_-2170228 id=-1458129621 kind=ROGUE_PLANET name=PGR--5002361.0.-5002361 starless + system -2063555_7189287_7920294 id=-225880809 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless + system -2140810_2039107_9279683 id=-243099345 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless + system -3336357_4475214_3033351 id=-629791769 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless + system -364554_6854024_-3395136 id=-180255469 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless + system -3941235_-1709038_3545575 id=-1370886497 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless + system -4666123_-580567_5682104 id=-1303466537 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless + system -554372_-4442061_-774973 id=-291544557 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless + system -626941_5154507_4503681 id=-789817773 kind=ROGUE_PLANET name=PGR--5002361.5002361.0 starless + system 1228651_-2557618_8861204 id=-548985509 kind=ROGUE_PLANET name=PGR-0.-5002361.5002361 starless + system 2274979_7150205_-1755432 id=-840467989 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless + system 2691671_5432625_7859903 id=-1879670169 kind=STAR name=PGS-0.5002361.5002361 starTemp=40 starSize=0.8387366533279419 + system 3486529_6795101_617493 id=-1428201685 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless + system 3662998_191651_-1822417 id=-41240637 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless + system 3744070_1703327_2446882 id=-342178325 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 4347096_3736058_9064309 id=-439396409 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless + system 528043_-2545788_1919161 id=-1278118149 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless + system 5948611_6639061_7642905 id=-775167053 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless + system 6681053_-1258075_6889516 id=-557819189 kind=ROGUE_PLANET name=PGR-5002361.-5002361.5002361 starless + system 6822102_5146281_2692793 id=-652918041 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless + system 7474804_-1243737_1670574 id=-176624233 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless + system 756689_-1996515_-853506 id=-1395994849 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless + system 7769045_2585852_903758 id=-1675040469 kind=STAR name=PGS-5002361.0.0 starTemp=40 starSize=0.8881509304046631 + system 8273292_573611_-2696544 id=-1739755397 kind=ROGUE_PLANET name=PGR-5002361.0.-5002361 starless + system 8446414_4393296_7956124 id=-264084353 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless + system 8839150_8100443_-3684508 id=-91668937 kind=ROGUE_PLANET name=PGR-5002361.5002361.-5002361 starless + system 8988904_-910550_-4036263 id=-1207384369 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless +seed 8675309 systems=27 + body -1440306_4321771_-1644504 -1440306_4321771_-1644504 kind=MOON orbit=0 radius=0.6918311989713263 starId=-224544461 frame=false + body -1440306_4321771_-1644504 -1440306_4321771_-1644504 kind=MOON orbit=0 radius=1.4486217500512495 starId=-224544461 frame=false + body -1440306_4321771_-1644504 -1440306_4321771_-1644504 kind=ROGUE_PLANET orbit=0 radius=1.858589898702767 starId=-224544461 frame=true + body -1487091_5789006_3108422 -1487091_5789006_3108422 kind=ROGUE_PLANET orbit=0 radius=0.7622212449935264 starId=-1737974265 frame=true + body -2182872_2271086_929819 -2182872_2271086_929819 kind=ROGUE_PLANET orbit=0 radius=1.5594868722814699 starId=-852504905 frame=true + body -2532019_1322888_8152935 -2532019_1322888_8152935 kind=ROGUE_PLANET orbit=0 radius=1.95774137431041 starId=-1880339145 frame=true + body -2648150_9876469_-4697214 -2648150_9876469_-4697214 kind=MOON orbit=0 radius=0.3073465246954764 starId=-337840993 frame=false + body -2648150_9876469_-4697214 -2648150_9876469_-4697214 kind=ROGUE_PLANET orbit=0 radius=1.8158165163169306 starId=-337840993 frame=true + body -3715368_-3025898_464323 -3715067_-3025894_464264 kind=MOON orbit=1638 radius=0.20567681577882893 starId=-351188325 frame=false + body -3715368_-3025898_464323 -3715067_-3025894_464264 kind=PLANET orbit=1638 radius=1.1310343685083477 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715248_-3025896_464384 kind=PLANET orbit=718 radius=2.3765942402912463 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715284_-3025898_464806 kind=ASTEROID_BELT orbit=2620 radius=0.0 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715331_-3025899_464308 kind=ASTEROID_BELT orbit=216 radius=0.0 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715334_-3025900_464348 kind=MOON orbit=223 radius=0.47505439226056184 starId=-351188325 frame=false + body -3715368_-3025898_464323 -3715334_-3025900_464348 kind=PLANET orbit=223 radius=0.28266309604664974 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715355_-3025899_464334 kind=MOON orbit=90 radius=0.41037160072533463 starId=-351188325 frame=false + body -3715368_-3025898_464323 -3715355_-3025899_464334 kind=PLANET orbit=90 radius=2.131531437678731 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715364_-3025898_464319 kind=PLANET orbit=31 radius=2.2697636812476105 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715366_-3025898_464319 kind=PLANET orbit=22 radius=0.23651511772733647 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715368_-3025898_464323 kind=STAR orbit=0 radius=0.0 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715370_-3025898_464323 kind=MOON orbit=12 radius=0.200038276016525 starId=-351188325 frame=false + body -3715368_-3025898_464323 -3715370_-3025898_464323 kind=MOON orbit=12 radius=0.21441160139738466 starId=-351188325 frame=false + body -3715368_-3025898_464323 -3715370_-3025898_464323 kind=PLANET orbit=12 radius=0.20180455276641018 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715378_-3025898_464329 kind=PLANET orbit=61 radius=0.8226351871084372 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715380_-3025901_464395 kind=GAS_GIANT orbit=389 radius=8.241293476987526 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715380_-3025901_464395 kind=MOON orbit=389 radius=0.47326136522912626 starId=-351188325 frame=false + body -3715368_-3025898_464323 -3715386_-3025895_464510 kind=GAS_GIANT orbit=1007 radius=3.104307714932146 starId=-351188325 frame=true + body -3715368_-3025898_464323 -3715386_-3025895_464510 kind=MOON orbit=1007 radius=0.2219279920272386 starId=-351188325 frame=false + body -3715368_-3025898_464323 -3715391_-3025898_464332 kind=PLANET orbit=130 radius=0.7953404597424061 starId=-351188325 frame=true + body -4100505_9119936_7447874 -4100505_9119936_7447874 kind=MOON orbit=0 radius=0.41094175801147914 starId=-1795081381 frame=false + body -4100505_9119936_7447874 -4100505_9119936_7447874 kind=ROGUE_PLANET orbit=0 radius=0.20249000003811363 starId=-1795081381 frame=true + body -628503_-2730462_8503404 -628503_-2730462_8503404 kind=MOON orbit=0 radius=0.8004976006437003 starId=-91454269 frame=false + body -628503_-2730462_8503404 -628503_-2730462_8503404 kind=ROGUE_PLANET orbit=0 radius=0.22475689697930112 starId=-91454269 frame=true + body -660880_-1054453_-895113 -660880_-1054453_-895113 kind=MOON orbit=0 radius=0.3744101559829215 starId=-1645210425 frame=false + body -660880_-1054453_-895113 -660880_-1054453_-895113 kind=MOON orbit=0 radius=0.4470232669285201 starId=-1645210425 frame=false + body -660880_-1054453_-895113 -660880_-1054453_-895113 kind=ROGUE_PLANET orbit=0 radius=0.5611689416997312 starId=-1645210425 frame=true + body 1268352_-472382_-2204417 1268259_-472378_-2204392 kind=ASTEROID_BELT orbit=515 radius=0.0 starId=-636287673 frame=true + body 1268352_-472382_-2204417 1268292_-472381_-2204414 kind=PLANET orbit=322 radius=0.6452755512774496 starId=-636287673 frame=true + body 1268352_-472382_-2204417 1268336_-472382_-2204397 kind=MOON orbit=139 radius=0.2470380566163046 starId=-636287673 frame=false + body 1268352_-472382_-2204417 1268336_-472382_-2204397 kind=PLANET orbit=139 radius=0.2165813118510709 starId=-636287673 frame=true + body 1268352_-472382_-2204417 1268344_-472382_-2204418 kind=GAS_GIANT orbit=42 radius=4.101154169118654 starId=-636287673 frame=true + body 1268352_-472382_-2204417 1268344_-472382_-2204418 kind=MOON orbit=42 radius=0.5585556554994643 starId=-636287673 frame=false + body 1268352_-472382_-2204417 1268350_-472382_-2204421 kind=ASTEROID_BELT orbit=23 radius=0.0 starId=-636287673 frame=true + body 1268352_-472382_-2204417 1268351_-472382_-2204415 kind=MOON orbit=11 radius=0.37683912902283956 starId=-636287673 frame=false + body 1268352_-472382_-2204417 1268351_-472382_-2204415 kind=MOON orbit=11 radius=0.5159283799812471 starId=-636287673 frame=false + body 1268352_-472382_-2204417 1268351_-472382_-2204415 kind=PLANET orbit=11 radius=0.5506753050718656 starId=-636287673 frame=true + body 1268352_-472382_-2204417 1268352_-472382_-2204417 kind=STAR orbit=0 radius=0.0 starId=-636287673 frame=true + body 1268352_-472382_-2204417 1268355_-472382_-2204418 kind=MOON orbit=17 radius=0.47086380378858367 starId=-636287673 frame=false + body 1268352_-472382_-2204417 1268355_-472382_-2204418 kind=MOON orbit=17 radius=0.6080086952896047 starId=-636287673 frame=false + body 1268352_-472382_-2204417 1268355_-472382_-2204418 kind=PLANET orbit=17 radius=1.203246167178117 starId=-636287673 frame=true + body 1315331_2596741_5713669 1315331_2596741_5713669 kind=MOON orbit=0 radius=1.0395123660391348 starId=-1517147849 frame=false + body 1315331_2596741_5713669 1315331_2596741_5713669 kind=MOON orbit=0 radius=1.8304825202249364 starId=-1517147849 frame=false + body 1315331_2596741_5713669 1315331_2596741_5713669 kind=ROGUE_PLANET orbit=0 radius=1.8909505397829076 starId=-1517147849 frame=true + body 1407594_9783198_-2449441 1407594_9783198_-2449441 kind=MOON orbit=0 radius=1.7372995652657248 starId=-1372461905 frame=false + body 1407594_9783198_-2449441 1407594_9783198_-2449441 kind=ROGUE_PLANET orbit=0 radius=0.47103298828651424 starId=-1372461905 frame=true + body 1673725_3372175_3108455 1673725_3372175_3108455 kind=ROGUE_PLANET orbit=0 radius=0.391336859643731 starId=-576770217 frame=true + body 1881534_-3011027_5110456 1881534_-3011027_5110456 kind=ROGUE_PLANET orbit=0 radius=2.359438373013696 starId=-1880844221 frame=true + body 2710691_-1047310_454879 2710691_-1047310_454879 kind=ROGUE_PLANET orbit=0 radius=6.180808030437285 starId=-1195104453 frame=true + body 3368241_5255982_7050837 3368241_5255982_7050837 kind=ROGUE_PLANET orbit=0 radius=0.3034346821187427 starId=-326311765 frame=true + body 4060263_3898666_-2963467 4060263_3898666_-2963467 kind=ROGUE_PLANET orbit=0 radius=0.42794466068253867 starId=-1719884493 frame=true + body 499372_6052057_2026466 499372_6052057_2026466 kind=ROGUE_PLANET orbit=0 radius=1.4507643648065525 starId=-1348043589 frame=true + body 5392052_6211062_-1062868 5391721_6211077_-1062265 kind=ASTEROID_BELT orbit=3678 radius=0.0 starId=-1954607381 frame=true + body 5392052_6211062_-1062868 5391982_6211059_-1062444 kind=PLANET orbit=2299 radius=0.27541638291159487 starId=-1954607381 frame=true + body 5392052_6211062_-1062868 5392042_6211062_-1062878 kind=MOON orbit=77 radius=0.24439099491676247 starId=-1954607381 frame=false + body 5392052_6211062_-1062868 5392042_6211062_-1062878 kind=MOON orbit=77 radius=0.3102458262031239 starId=-1954607381 frame=false + body 5392052_6211062_-1062868 5392042_6211062_-1062878 kind=PLANET orbit=77 radius=2.3911871875797917 starId=-1954607381 frame=true + body 5392052_6211062_-1062868 5392052_6211062_-1062868 kind=STAR orbit=0 radius=0.0 starId=-1954607381 frame=true + body 5392052_6211062_-1062868 5392095_6211060_-1062881 kind=PLANET orbit=240 radius=0.7707202888918347 starId=-1954607381 frame=true + body 5504536_9800924_3923283 5504515_9800927_3923212 kind=GAS_GIANT orbit=398 radius=3.171788138456746 starId=-1100599953 frame=true + body 5504536_9800924_3923283 5504515_9800927_3923212 kind=MOON orbit=398 radius=0.22933661208386558 starId=-1100599953 frame=false + body 5504536_9800924_3923283 5504515_9800927_3923212 kind=MOON orbit=398 radius=0.31451081886642485 starId=-1100599953 frame=false + body 5504536_9800924_3923283 5504515_9800927_3923212 kind=MOON orbit=398 radius=0.35097569265251594 starId=-1100599953 frame=false + body 5504536_9800924_3923283 5504536_9800924_3923283 kind=STAR orbit=0 radius=0.0 starId=-1100599953 frame=true + body 5504536_9800924_3923283 5504537_9800924_3923282 kind=MOON orbit=7 radius=0.40009045843515056 starId=-1100599953 frame=false + body 5504536_9800924_3923283 5504537_9800924_3923282 kind=MOON orbit=7 radius=0.7146264002883986 starId=-1100599953 frame=false + body 5504536_9800924_3923283 5504537_9800924_3923282 kind=PLANET orbit=7 radius=1.7986200380985606 starId=-1100599953 frame=true + body 5504536_9800924_3923283 5504538_9800924_3923276 kind=MOON orbit=40 radius=0.3179143781697107 starId=-1100599953 frame=false + body 5504536_9800924_3923283 5504538_9800924_3923276 kind=MOON orbit=40 radius=0.6523342704190511 starId=-1100599953 frame=false + body 5504536_9800924_3923283 5504538_9800924_3923276 kind=PLANET orbit=40 radius=0.4069949825213218 starId=-1100599953 frame=true + body 5504536_9800924_3923283 5504548_9800924_3923322 kind=ASTEROID_BELT orbit=221 radius=0.0 starId=-1100599953 frame=true + body 5504536_9800924_3923283 5504552_9800925_3923300 kind=PLANET orbit=126 radius=0.35701423058093684 starId=-1100599953 frame=true + body 5504536_9800924_3923283 5504644_9800924_3923234 kind=ASTEROID_BELT orbit=636 radius=0.0 starId=-1100599953 frame=true + body 6282550_3920081_-4327174 6282550_3920081_-4327174 kind=ROGUE_PLANET orbit=0 radius=0.20476677664862225 starId=-1926330197 frame=true + body 7614190_8929461_6983314 7614190_8929461_6983314 kind=ROGUE_PLANET orbit=0 radius=0.3244906791992829 starId=-397610089 frame=true + body 7821430_3964300_8607040 7821430_3964300_8607040 kind=MOON orbit=0 radius=0.9204718241145495 starId=-979945297 frame=false + body 7821430_3964300_8607040 7821430_3964300_8607040 kind=ROGUE_PLANET orbit=0 radius=0.5873505561269114 starId=-979945297 frame=true + body 8556534_-4759325_7276386 8556534_-4759325_7276386 kind=MOON orbit=0 radius=0.21529441503824975 starId=-1513594621 frame=false + body 8556534_-4759325_7276386 8556534_-4759325_7276386 kind=MOON orbit=0 radius=0.3315137311013633 starId=-1513594621 frame=false + body 8556534_-4759325_7276386 8556534_-4759325_7276386 kind=ROGUE_PLANET orbit=0 radius=0.25120702885343593 starId=-1513594621 frame=true + body 8869249_4721320_3066528 8869249_4721320_3066528 kind=MOON orbit=0 radius=2.260316585289282 starId=-893649661 frame=false + body 8869249_4721320_3066528 8869249_4721320_3066528 kind=ROGUE_PLANET orbit=0 radius=1.0153541787850344 starId=-893649661 frame=true + body 9342137_-1318710_-2154197 9342137_-1318710_-2154197 kind=MOON orbit=0 radius=0.7561660356829851 starId=-364163833 frame=false + body 9342137_-1318710_-2154197 9342137_-1318710_-2154197 kind=MOON orbit=0 radius=1.6234315866951836 starId=-364163833 frame=false + body 9342137_-1318710_-2154197 9342137_-1318710_-2154197 kind=ROGUE_PLANET orbit=0 radius=0.6714008663449185 starId=-364163833 frame=true + body 9459338_-4115184_3189023 9459338_-4115184_3189023 kind=ROGUE_PLANET orbit=0 radius=0.9513578971220109 starId=-40991845 frame=true + derived -1440306_4321771_-1644504 -1440306_4321771_-1644504 type=ice mass=10.377861862102423 radius=1.858589898702767 gravity=300 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=19065 metallicity=0.6057706896698947 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1487091_5789006_3108422 -1487091_5789006_3108422 type=barren mass=0.45371319611386035 radius=0.7622212449935264 gravity=78 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=40969 metallicity=1.1313068142223444 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2182872_2271086_929819 -2182872_2271086_929819 type=ice mass=4.930558943669398 radius=1.5594868722814699 gravity=203 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=14486 metallicity=0.8780061621167181 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2532019_1322888_8152935 -2532019_1322888_8152935 type=superearth mass=13.341765256018288 radius=1.95774137431041 gravity=348 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=42915 metallicity=1.463175358049182 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2648150_9876469_-4697214 -2648150_9876469_-4697214 type=superearth mass=11.122553864105102 radius=1.8158165163169306 gravity=337 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=15076 metallicity=1.0301874894482133 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715067_-3025894_464264 type=ice mass=1.9202967701603253 radius=1.1310343685083477 gravity=150 pressure=1600 tempK=85 oxygen=false locked=false rings=false rotation=13803 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715248_-3025896_464384 type=ice mass=29.938849817208148 radius=2.3765942402912463 gravity=400 pressure=1600 tempK=129 oxygen=false locked=false rings=false rotation=57034 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715284_-3025898_464806 type=ice mass=1.9117522949109045 radius=1.1230195340795335 gravity=152 pressure=1600 tempK=67 oxygen=false locked=false rings=false rotation=13796 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715331_-3025899_464308 type=exotic mass=2.1019781855337327 radius=1.2170316663420997 gravity=142 pressure=1600 tempK=271 oxygen=false locked=false rings=false rotation=17356 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715334_-3025900_464348 type=ice mass=0.010144834480563766 radius=0.28266309604664974 gravity=13 pressure=0 tempK=103 oxygen=false locked=false rings=false rotation=39841 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715355_-3025899_464334 type=superearth mass=14.907317191154986 radius=2.131531437678731 gravity=328 pressure=1600 tempK=420 oxygen=false locked=false rings=false rotation=29565 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715364_-3025898_464319 type=greenhouse mass=19.769566824621222 radius=2.2697636812476105 gravity=384 pressure=1600 tempK=554 oxygen=false locked=true rings=false rotation=77807 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715366_-3025898_464319 type=desert mass=0.005334838248948286 radius=0.23651511772733647 gravity=10 pressure=0 tempK=378 oxygen=false locked=true rings=false rotation=39327 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715368_-3025898_464323 type=lava mass=0.0033230152860885755 radius=0.2023556097717985 gravity=8 pressure=0 tempK=1889 oxygen=false locked=true rings=false rotation=21593 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715370_-3025898_464323 type=barren mass=0.0027917778218846064 radius=0.20180455276641018 gravity=7 pressure=0 tempK=542 oxygen=false locked=true rings=false rotation=6357 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715378_-3025898_464329 type=ice mass=0.37963888245778626 radius=0.8226351871084372 gravity=56 pressure=31 tempK=197 oxygen=false locked=false rings=false rotation=13777 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715380_-3025901_464395 type=gasgiant mass=163.68667159513973 radius=8.241293476987526 gravity=241 pressure=1600 tempK=186 oxygen=false locked=false rings=false rotation=7948 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715386_-3025895_464510 type=icegiant mass=17.327779061480165 radius=3.104307714932146 gravity=180 pressure=1600 tempK=115 oxygen=false locked=false rings=true rotation=6186 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3715368_-3025898_464323 -3715391_-3025898_464332 type=ice mass=0.3974007358220052 radius=0.7953404597424061 gravity=63 pressure=156 tempK=170 oxygen=false locked=false rings=false rotation=9797 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4100505_9119936_7447874 -4100505_9119936_7447874 type=ice mass=0.002925677165718173 radius=0.20249000003811363 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=53423 metallicity=1.112540504699406 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -628503_-2730462_8503404 -628503_-2730462_8503404 type=barren mass=0.0035874996009393812 radius=0.22475689697930112 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=91158 metallicity=0.49615603963499344 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -660880_-1054453_-895113 -660880_-1054453_-895113 type=barren mass=0.09756822926774975 radius=0.5611689416997312 gravity=31 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=41164 metallicity=1.2157740957062755 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1268352_-472382_-2204417 1268259_-472378_-2204392 type=superearth mass=7.927755523878962 radius=1.6586029986911508 gravity=288 pressure=1600 tempK=85 oxygen=false locked=false rings=false rotation=48149 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1268352_-472382_-2204417 1268292_-472381_-2204414 type=ice mass=0.1978383843924024 radius=0.6452755512774496 gravity=48 pressure=122 tempK=49 oxygen=false locked=false rings=false rotation=41315 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1268352_-472382_-2204417 1268336_-472382_-2204397 type=ice mass=0.00345957043996053 radius=0.2165813118510709 gravity=7 pressure=0 tempK=63 oxygen=false locked=false rings=false rotation=25031 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1268352_-472382_-2204417 1268344_-472382_-2204418 type=gasgiant mass=32.8781947501483 radius=4.101154169118654 gravity=195 pressure=1600 tempK=274 oxygen=false locked=false rings=true rotation=6488 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1268352_-472382_-2204417 1268350_-472382_-2204421 type=gasgiant mass=52.838738780413046 radius=5.040697686360118 gravity=208 pressure=1600 tempK=371 oxygen=false locked=false rings=false rotation=9002 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1268352_-472382_-2204417 1268351_-472382_-2204415 type=desert mass=0.1297786978767138 radius=0.5506753050718656 gravity=43 pressure=6 tempK=259 oxygen=false locked=true rings=false rotation=80284 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1268352_-472382_-2204417 1268352_-472382_-2204417 type=superearth mass=4.447393609080987 radius=1.4777327469738462 gravity=204 pressure=44 tempK=860 oxygen=false locked=true rings=false rotation=17966 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1268352_-472382_-2204417 1268355_-472382_-2204418 type=greenhouse mass=1.9643183536374995 radius=1.203246167178117 gravity=136 pressure=1600 tempK=363 oxygen=false locked=true rings=false rotation=8535 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1315331_2596741_5713669 1315331_2596741_5713669 type=ice mass=12.934325599371203 radius=1.8909505397829076 gravity=362 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=13371 metallicity=0.8349568742803939 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1407594_9783198_-2449441 1407594_9783198_-2449441 type=barren mass=0.058516468193337154 radius=0.47103298828651424 gravity=26 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=18840 metallicity=0.40428506216328813 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1673725_3372175_3108455 1673725_3372175_3108455 type=ice mass=0.03112067865715841 radius=0.391336859643731 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=63128 metallicity=1.2085371618398715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1881534_-3011027_5110456 1881534_-3011027_5110456 type=superearth mass=29.429310866728557 radius=2.359438373013696 gravity=400 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=12431 metallicity=1.4280539746788947 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2710691_-1047310_454879 2710691_-1047310_454879 type=gasgiant mass=84.45551815291512 radius=6.180808030437285 gravity=221 pressure=1600 tempK=43 oxygen=false locked=false rings=true rotation=6678 metallicity=0.84226801458673 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3368241_5255982_7050837 3368241_5255982_7050837 type=ice mass=0.013454795033272725 radius=0.3034346821187427 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=36979 metallicity=0.48929816684250027 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4060263_3898666_-2963467 4060263_3898666_-2963467 type=barren mass=0.05147967174733844 radius=0.42794466068253867 gravity=28 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=9260 metallicity=1.5423537513918921 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 499372_6052057_2026466 499372_6052057_2026466 type=ice mass=4.644453342051514 radius=1.4507643648065525 gravity=221 pressure=0 tempK=43 oxygen=false locked=false rings=false rotation=19002 metallicity=0.8983108650169582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5392052_6211062_-1062868 5391721_6211077_-1062265 type=superearth mass=8.681226927094112 radius=1.7870233158155744 gravity=272 pressure=1600 tempK=105 oxygen=false locked=false rings=false rotation=7206 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5392052_6211062_-1062868 5391982_6211059_-1062444 type=barren mass=0.009913430355922298 radius=0.27541638291159487 gravity=13 pressure=1 tempK=62 oxygen=false locked=false rings=false rotation=54635 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5392052_6211062_-1062868 5392042_6211062_-1062878 type=superearth mass=22.348141890617914 radius=2.3911871875797917 gravity=391 pressure=1600 tempK=726 oxygen=false locked=false rings=false rotation=28856 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5392052_6211062_-1062868 5392052_6211062_-1062868 type=lava mass=2.6109467287345822 radius=1.2905541091838826 gravity=157 pressure=1 tempK=3018 oxygen=false locked=true rings=false rotation=25951 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5392052_6211062_-1062868 5392095_6211060_-1062881 type=ice mass=0.30143691962074926 radius=0.7707202888918347 gravity=51 pressure=51 tempK=159 oxygen=false locked=false rings=false rotation=77653 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5504536_9800924_3923283 5504515_9800927_3923212 type=gasgiant mass=18.206377216773813 radius=3.171788138456746 gravity=181 pressure=1600 tempK=96 oxygen=false locked=false rings=false rotation=4863 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5504536_9800924_3923283 5504536_9800924_3923283 type=barren mass=0.08859859371409555 radius=0.4914168748804031 gravity=37 pressure=0 tempK=985 oxygen=false locked=true rings=false rotation=14638 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5504536_9800924_3923283 5504537_9800924_3923282 type=lava mass=10.705415494759238 radius=1.7986200380985606 gravity=331 pressure=1600 tempK=842 oxygen=false locked=true rings=false rotation=9773 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5504536_9800924_3923283 5504538_9800924_3923276 type=barren mass=0.030448746587449775 radius=0.4069949825213218 gravity=18 pressure=1 tempK=155 oxygen=false locked=true rings=false rotation=40600 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5504536_9800924_3923283 5504548_9800924_3923322 type=icegiant mass=202.20009831621877 radius=9.034300451641911 gravity=248 pressure=1600 tempK=129 oxygen=false locked=false rings=true rotation=5217 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5504536_9800924_3923283 5504552_9800925_3923300 type=ice mass=0.022887226912599735 radius=0.35701423058093684 gravity=18 pressure=1 tempK=72 oxygen=false locked=false rings=false rotation=23388 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5504536_9800924_3923283 5504644_9800924_3923234 type=barren mass=0.034213554407382614 radius=0.38774183430029346 gravity=23 pressure=5 tempK=39 oxygen=false locked=false rings=false rotation=66550 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6282550_3920081_-4327174 6282550_3920081_-4327174 type=barren mass=0.002877051787226516 radius=0.20476677664862225 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=30118 metallicity=0.6453043137367989 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7614190_8929461_6983314 7614190_8929461_6983314 type=barren mass=0.012124284240218575 radius=0.3244906791992829 gravity=12 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=78290 metallicity=1.386929789193736 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7821430_3964300_8607040 7821430_3964300_8607040 type=barren mass=0.13119556478427508 radius=0.5873505561269114 gravity=38 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=47532 metallicity=0.6269401359278104 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8556534_-4759325_7276386 8556534_-4759325_7276386 type=barren mass=0.006142163713278197 radius=0.25120702885343593 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=76639 metallicity=1.0565819982317461 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8869249_4721320_3066528 8869249_4721320_3066528 type=barren mass=0.845801395899521 radius=1.0153541787850344 gravity=82 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=25248 metallicity=1.2929327969937456 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9342137_-1318710_-2154197 9342137_-1318710_-2154197 type=barren mass=0.21029229819468406 radius=0.6714008663449185 gravity=47 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=19850 metallicity=0.8363712659150969 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9459338_-4115184_3189023 9459338_-4115184_3189023 type=ice mass=0.7470096901599058 radius=0.9513578971220109 gravity=83 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=47356 metallicity=0.601570911220727 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1440306_4321771_-1644504 id=-224544461 kind=ROGUE_PLANET name=PGR--5002361.0.-5002361 starless + system -1487091_5789006_3108422 id=-1737974265 kind=ROGUE_PLANET name=PGR--5002361.5002361.0 starless + system -2182872_2271086_929819 id=-852504905 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless + system -2532019_1322888_8152935 id=-1880339145 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless + system -2648150_9876469_-4697214 id=-337840993 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless + system -3715368_-3025898_464323 id=-351188325 kind=STAR name=PGS--5002361.-5002361.0 starTemp=70 starSize=0.9819875955581665 + system -4100505_9119936_7447874 id=-1795081381 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless + system -628503_-2730462_8503404 id=-91454269 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless + system -660880_-1054453_-895113 id=-1645210425 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless + system 1268352_-472382_-2204417 id=-636287673 kind=STAR name=PGS-0.-5002361.-5002361 starTemp=40 starSize=0.7072588205337524 + system 1315331_2596741_5713669 id=-1517147849 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless + system 1407594_9783198_-2449441 id=-1372461905 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless + system 1673725_3372175_3108455 id=-576770217 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 1881534_-3011027_5110456 id=-1880844221 kind=ROGUE_PLANET name=PGR-0.-5002361.5002361 starless + system 2710691_-1047310_454879 id=-1195104453 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless + system 3368241_5255982_7050837 id=-326311765 kind=ROGUE_PLANET name=PGR-0.5002361.5002361 starless + system 4060263_3898666_-2963467 id=-1719884493 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless + system 499372_6052057_2026466 id=-1348043589 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless + system 5392052_6211062_-1062868 id=-1954607381 kind=STAR name=PGS-5002361.5002361.-5002361 starTemp=100 starSize=1.2276172637939453 + system 5504536_9800924_3923283 id=-1100599953 kind=STAR name=PGS-5002361.5002361.0 starTemp=40 starSize=0.827074408531189 + system 6282550_3920081_-4327174 id=-1926330197 kind=ROGUE_PLANET name=PGR-5002361.0.-5002361 starless + system 7614190_8929461_6983314 id=-397610089 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless + system 7821430_3964300_8607040 id=-979945297 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless + system 8556534_-4759325_7276386 id=-1513594621 kind=ROGUE_PLANET name=PGR-5002361.-5002361.5002361 starless + system 8869249_4721320_3066528 id=-893649661 kind=ROGUE_PLANET name=PGR-5002361.0.0 starless + system 9342137_-1318710_-2154197 id=-364163833 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless + system 9459338_-4115184_3189023 id=-40991845 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless +seed -1 systems=27 + body -1318696_8986598_-3915037 -1317886_8986631_-3914953 kind=ASTEROID_BELT orbit=4356 radius=0.0 starId=-905285817 frame=true + body -1318696_8986598_-3915037 -1318191_8986622_-3914975 kind=GAS_GIANT orbit=2723 radius=9.166746324818572 starId=-905285817 frame=true + body -1318696_8986598_-3915037 -1318607_8986594_-3915071 kind=GAS_GIANT orbit=512 radius=10.777387057228022 starId=-905285817 frame=true + body -1318696_8986598_-3915037 -1318607_8986594_-3915071 kind=MOON orbit=512 radius=0.23392988329428063 starId=-905285817 frame=false + body -1318696_8986598_-3915037 -1318607_8986594_-3915071 kind=MOON orbit=512 radius=0.32810033747221057 starId=-905285817 frame=false + body -1318696_8986598_-3915037 -1318607_8986594_-3915071 kind=MOON orbit=512 radius=0.5111663133840638 starId=-905285817 frame=false + body -1318696_8986598_-3915037 -1318665_8986600_-3914994 kind=ASTEROID_BELT orbit=284 radius=0.0 starId=-905285817 frame=true + body -1318696_8986598_-3915037 -1318688_8986598_-3915034 kind=PLANET orbit=44 radius=0.9968042979223011 starId=-905285817 frame=true + body -1318696_8986598_-3915037 -1318696_8986598_-3915037 kind=STAR orbit=0 radius=0.0 starId=-905285817 frame=true + body -1318696_8986598_-3915037 -1318724_8986598_-3915032 kind=STAR orbit=152 radius=91.55521749079227 starId=-905285818 frame=true + body -1353458_-2041995_3669964 -1353458_-2041995_3669964 kind=MOON orbit=0 radius=0.5324815188458033 starId=-1793186305 frame=false + body -1353458_-2041995_3669964 -1353458_-2041995_3669964 kind=MOON orbit=0 radius=1.4461590339975599 starId=-1793186305 frame=false + body -1353458_-2041995_3669964 -1353458_-2041995_3669964 kind=ROGUE_PLANET orbit=0 radius=0.9643703406893043 starId=-1793186305 frame=true + body -1504868_3859101_7238142 -1504868_3859101_7238142 kind=ROGUE_PLANET orbit=0 radius=0.2032533764928848 starId=-731140245 frame=true + body -1702786_5224077_9097589 -1702786_5224077_9097589 kind=MOON orbit=0 radius=0.3763027176750806 starId=-1673402021 frame=false + body -1702786_5224077_9097589 -1702786_5224077_9097589 kind=ROGUE_PLANET orbit=0 radius=1.742200393255895 starId=-1673402021 frame=true + body -2157794_-3988883_-1548825 -2157794_-3988883_-1548825 kind=ROGUE_PLANET orbit=0 radius=0.2269790512853808 starId=-1640872829 frame=true + body -362968_4023206_-1712631 -362968_4023206_-1712631 kind=ROGUE_PLANET orbit=0 radius=0.20672927136756797 starId=-429342865 frame=true + body -4110886_-2675466_7716656 -4110886_-2675466_7716656 kind=ROGUE_PLANET orbit=0 radius=0.8552568554482771 starId=-305214521 frame=true + body -4807070_1870953_3722184 -4807070_1870953_3722184 kind=MOON orbit=0 radius=1.1039665790614814 starId=-796796053 frame=false + body -4807070_1870953_3722184 -4807070_1870953_3722184 kind=ROGUE_PLANET orbit=0 radius=1.2754301329722684 starId=-796796053 frame=true + body -914407_6480482_4740872 -914397_6480482_4740869 kind=PLANET orbit=55 radius=1.752767775052126 starId=-1744473825 frame=true + body -914407_6480482_4740872 -914407_6480482_4740872 kind=STAR orbit=0 radius=0.0 starId=-1744473825 frame=true + body -914407_6480482_4740872 -914411_6480482_4740906 kind=STAR orbit=185 radius=103.22470710575581 starId=-1744473826 frame=true + body -914407_6480482_4740872 -914426_6480521_4740081 kind=ASTEROID_BELT orbit=4236 radius=0.0 starId=-1744473825 frame=true + body -914407_6480482_4740872 -914619_6480479_4740425 kind=PLANET orbit=2648 radius=1.3312106788774432 starId=-1744473825 frame=true + body 102729_-4691975_-1824860 102729_-4691975_-1824860 kind=MOON orbit=0 radius=0.4858620267121579 starId=-986449737 frame=false + body 102729_-4691975_-1824860 102729_-4691975_-1824860 kind=ROGUE_PLANET orbit=0 radius=1.2514467561785527 starId=-986449737 frame=true + body 1386765_1777561_3866474 1386765_1777561_3866474 kind=MOON orbit=0 radius=0.7692105872285928 starId=-1806957165 frame=false + body 1386765_1777561_3866474 1386765_1777561_3866474 kind=ROGUE_PLANET orbit=0 radius=0.6469067641925179 starId=-1806957165 frame=true + body 1414282_6629949_2442943 1414282_6629949_2442943 kind=MOON orbit=0 radius=0.6090133131739475 starId=-1333921245 frame=false + body 1414282_6629949_2442943 1414282_6629949_2442943 kind=MOON orbit=0 radius=2.357964579717853 starId=-1333921245 frame=false + body 1414282_6629949_2442943 1414282_6629949_2442943 kind=ROGUE_PLANET orbit=0 radius=0.2000303552532779 starId=-1333921245 frame=true + body 1932863_-2490869_1402243 1932863_-2490869_1402243 kind=ROGUE_PLANET orbit=0 radius=1.4336462116033544 starId=-681598609 frame=true + body 2030702_-209809_5919874 2028640_-209750_5920429 kind=GAS_GIANT orbit=11425 radius=3.8694011905491568 starId=-129286269 frame=true + body 2030702_-209809_5919874 2029022_-209735_5916898 kind=ASTEROID_BELT orbit=18280 radius=0.0 starId=-129286269 frame=true + body 2030702_-209809_5919874 2030246_-209824_5920264 kind=ASTEROID_BELT orbit=3210 radius=0.0 starId=-129286269 frame=true + body 2030702_-209809_5919874 2030430_-209815_5919472 kind=PLANET orbit=2597 radius=1.5652167800475636 starId=-129286269 frame=true + body 2030702_-209809_5919874 2030438_-209803_5918826 kind=GAS_GIANT orbit=5778 radius=9.482777391861841 starId=-129286269 frame=true + body 2030702_-209809_5919874 2030438_-209803_5918826 kind=MOON orbit=5778 radius=0.23078417705442864 starId=-129286269 frame=false + body 2030702_-209809_5919874 2030438_-209803_5918826 kind=MOON orbit=5778 radius=0.5140558521282359 starId=-129286269 frame=false + body 2030702_-209809_5919874 2030578_-209804_5919889 kind=PLANET orbit=666 radius=1.903584469464533 starId=-129286269 frame=true + body 2030702_-209809_5919874 2030603_-209804_5920078 kind=MOON orbit=1212 radius=0.23597330481517403 starId=-129286269 frame=false + body 2030702_-209809_5919874 2030603_-209804_5920078 kind=PLANET orbit=1212 radius=0.40667920422127396 starId=-129286269 frame=true + body 2030702_-209809_5919874 2030688_-209810_5919897 kind=MOON orbit=147 radius=0.5243609414710944 starId=-129286269 frame=false + body 2030702_-209809_5919874 2030688_-209810_5919897 kind=PLANET orbit=147 radius=2.249378149861664 starId=-129286269 frame=true + body 2030702_-209809_5919874 2030695_-209809_5919888 kind=PLANET orbit=85 radius=2.4215629335179583 starId=-129286269 frame=true + body 2030702_-209809_5919874 2030702_-209809_5919874 kind=STAR orbit=0 radius=0.0 starId=-129286269 frame=true + body 2030702_-209809_5919874 2030705_-209809_5919877 kind=STAR orbit=23 radius=73.34426656901836 starId=-129286270 frame=true + body 2030702_-209809_5919874 2030736_-209808_5919916 kind=MOON orbit=290 radius=0.22565649492379916 starId=-129286269 frame=false + body 2030702_-209809_5919874 2030736_-209808_5919916 kind=PLANET orbit=290 radius=1.3611153732714876 starId=-129286269 frame=true + body 2039398_8275570_9269915 2039398_8275570_9269915 kind=MOON orbit=0 radius=0.4610801615111488 starId=-1830124441 frame=false + body 2039398_8275570_9269915 2039398_8275570_9269915 kind=ROGUE_PLANET orbit=0 radius=0.5235138597237201 starId=-1830124441 frame=true + body 2241385_8907861_-1975542 2241385_8907861_-1975542 kind=ROGUE_PLANET orbit=0 radius=1.6405990388349718 starId=-704831901 frame=true + body 370568_3515329_-2658033 370568_3515329_-2658033 kind=ROGUE_PLANET orbit=0 radius=0.8124595416171261 starId=-1989327477 frame=true + body 4076707_1478856_6199021 4076254_1478945_6196302 kind=ASTEROID_BELT orbit=14750 radius=0.0 starId=-108865093 frame=true + body 4076707_1478856_6199021 4076349_1478856_6198826 kind=STAR orbit=2183 radius=116.66968788027764 starId=-108865094 frame=true + body 4076707_1478856_6199021 4076707_1478856_6199021 kind=STAR orbit=0 radius=0.0 starId=-108865093 frame=true + body 4076707_1478856_6199021 4076712_1478856_6199003 kind=PLANET orbit=102 radius=1.339988705282001 starId=-108865093 frame=true + body 4076707_1478856_6199021 4076783_1478853_6199018 kind=PLANET orbit=406 radius=2.462976278612129 starId=-108865093 frame=true + body 4076707_1478856_6199021 4078344_1478888_6198481 kind=MOON orbit=9219 radius=0.6633388777554012 starId=-108865093 frame=false + body 4076707_1478856_6199021 4078344_1478888_6198481 kind=PLANET orbit=9219 radius=0.3608682099209395 starId=-108865093 frame=true + body 5171696_5557639_1152562 5171696_5557639_1152562 kind=ROGUE_PLANET orbit=0 radius=2.1183410616555247 starId=-1586917565 frame=true + body 5699530_6314857_-235309 5699530_6314857_-235309 kind=ROGUE_PLANET orbit=0 radius=1.6736751477831422 starId=-1764830897 frame=true + body 5799956_-3218726_4876987 5799956_-3218726_4876987 kind=ROGUE_PLANET orbit=0 radius=0.44010709889146316 starId=-780232045 frame=true + body 6058862_2614790_2030893 6056728_2614916_2032616 kind=ASTEROID_BELT orbit=14684 radius=0.0 starId=-1811230377 frame=true + body 6058862_2614790_2030893 6058410_2614864_2029239 kind=GAS_GIANT orbit=9178 radius=3.389605866397197 starId=-1811230377 frame=true + body 6058862_2614790_2030893 6058500_2614795_2030763 kind=GAS_GIANT orbit=2059 radius=6.781850976788818 starId=-1811230377 frame=true + body 6058862_2614790_2030893 6058500_2614795_2030763 kind=MOON orbit=2059 radius=0.2028069061546957 starId=-1811230377 frame=false + body 6058862_2614790_2030893 6058500_2614795_2030763 kind=MOON orbit=2059 radius=0.21047105211003583 starId=-1811230377 frame=false + body 6058862_2614790_2030893 6058500_2614795_2030763 kind=MOON orbit=2059 radius=0.282696328177196 starId=-1811230377 frame=false + body 6058862_2614790_2030893 6058661_2614798_2030967 kind=ASTEROID_BELT orbit=1143 radius=0.0 starId=-1811230377 frame=true + body 6058862_2614790_2030893 6058862_2614790_2030893 kind=STAR orbit=0 radius=0.0 starId=-1811230377 frame=true + body 6058862_2614790_2030893 6058886_2614790_2030901 kind=PLANET orbit=136 radius=0.20651265210298247 starId=-1811230377 frame=true + body 6058862_2614790_2030893 6058957_2614792_2030875 kind=PLANET orbit=519 radius=0.3373745430786347 starId=-1811230377 frame=true + body 6643221_-3045831_-3292025 6643221_-3045831_-3292025 kind=ROGUE_PLANET orbit=0 radius=0.2071390155595182 starId=-383094489 frame=true + body 7759431_-1367342_9293025 7759431_-1367342_9293025 kind=MOON orbit=0 radius=0.25419963926621847 starId=-20326109 frame=false + body 7759431_-1367342_9293025 7759431_-1367342_9293025 kind=MOON orbit=0 radius=0.6283919111941731 starId=-20326109 frame=false + body 7759431_-1367342_9293025 7759431_-1367342_9293025 kind=ROGUE_PLANET orbit=0 radius=2.0339633844371137 starId=-20326109 frame=true + body 8695004_5647655_6839165 8695004_5647655_6839165 kind=MOON orbit=0 radius=0.20787758220781027 starId=-1415522037 frame=false + body 8695004_5647655_6839165 8695004_5647655_6839165 kind=MOON orbit=0 radius=0.35897766141027726 starId=-1415522037 frame=false + body 8695004_5647655_6839165 8695004_5647655_6839165 kind=ROGUE_PLANET orbit=0 radius=2.1417234039988013 starId=-1415522037 frame=true + body 9350867_3953928_9075861 9350867_3953928_9075861 kind=MOON orbit=0 radius=0.6264880419207874 starId=-1168331549 frame=false + body 9350867_3953928_9075861 9350867_3953928_9075861 kind=ROGUE_PLANET orbit=0 radius=0.9930186947160022 starId=-1168331549 frame=true + body 9450953_4739501_-3567012 9450953_4739501_-3567012 kind=MOON orbit=0 radius=1.3571081122505047 starId=-1579933465 frame=false + body 9450953_4739501_-3567012 9450953_4739501_-3567012 kind=MOON orbit=0 radius=2.2125930343742883 starId=-1579933465 frame=false + body 9450953_4739501_-3567012 9450953_4739501_-3567012 kind=ROGUE_PLANET orbit=0 radius=1.6547219994680025 starId=-1579933465 frame=true + derived -1318696_8986598_-3915037 -1317886_8986631_-3914953 type=ice mass=1.1403118528659018 radius=1.0610125397988572 gravity=101 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=58028 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1318696_8986598_-3915037 -1318191_8986622_-3914975 type=gasgiant mass=209.0831017618138 radius=9.166746324818572 gravity=249 pressure=1600 tempK=103 oxygen=false locked=false rings=true rotation=4875 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1318696_8986598_-3915037 -1318607_8986594_-3915071 type=gasgiant mass=303.39257559903837 radius=10.777387057228022 gravity=261 pressure=1600 tempK=239 oxygen=false locked=false rings=true rotation=9130 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1318696_8986598_-3915037 -1318665_8986600_-3914994 type=superearth mass=3.2060392517452287 radius=1.383941748505236 gravity=167 pressure=1600 tempK=349 oxygen=false locked=false rings=false rotation=39841 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1318696_8986598_-3915037 -1318688_8986598_-3915034 type=desert mass=0.925387669079765 radius=0.9968042979223011 gravity=93 pressure=30 tempK=394 oxygen=false locked=true rings=false rotation=54615 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1318696_8986598_-3915037 -1318696_8986598_-3915037 type=lava mass=9.532602606904776 radius=1.8239998144090523 gravity=287 pressure=21 tempK=2782 oxygen=false locked=true rings=false rotation=16399 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1318696_8986598_-3915037 -1318724_8986598_-3915032 type=greenhouse mass=13.76027159799629 radius=1.9359204601558124 gravity=367 pressure=1600 tempK=369 oxygen=false locked=false rings=false rotation=11197 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1353458_-2041995_3669964 -1353458_-2041995_3669964 type=ice mass=0.9705727911568036 radius=0.9643703406893043 gravity=104 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=45778 metallicity=1.1214926014420192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1504868_3859101_7238142 -1504868_3859101_7238142 type=ice mass=0.0023310606017554374 radius=0.2032533764928848 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=15869 metallicity=0.38446088590345784 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1702786_5224077_9097589 -1702786_5224077_9097589 type=superearth mass=8.285512268197653 radius=1.742200393255895 gravity=273 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=75847 metallicity=0.5813733269433281 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2157794_-3988883_-1548825 -2157794_-3988883_-1548825 type=ice mass=0.004825891880283745 radius=0.2269790512853808 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=72350 metallicity=0.6467485105947578 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -362968_4023206_-1712631 -362968_4023206_-1712631 type=barren mass=0.0034255630062420845 radius=0.20672927136756797 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=67718 metallicity=1.5756640897851817 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4110886_-2675466_7716656 -4110886_-2675466_7716656 type=ice mass=0.5433354695754443 radius=0.8552568554482771 gravity=74 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=14178 metallicity=1.5521124002335625 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4807070_1870953_3722184 -4807070_1870953_3722184 type=superearth mass=2.7887644245058842 radius=1.2754301329722684 gravity=171 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=8419 metallicity=1.4022218827767885 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -914407_6480482_4740872 -914397_6480482_4740869 type=superearth mass=9.878571415470441 radius=1.752767775052126 gravity=322 pressure=1600 tempK=613 oxygen=false locked=false rings=false rotation=36941 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -914407_6480482_4740872 -914407_6480482_4740872 type=lava mass=7.2416518225481585 radius=1.6596029611154552 gravity=263 pressure=240 tempK=1485 oxygen=false locked=true rings=false rotation=23655 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -914407_6480482_4740872 -914411_6480482_4740906 type=greenhouse mass=1.9521035208502402 radius=1.1875801386423888 gravity=138 pressure=904 tempK=348 oxygen=false locked=false rings=false rotation=74733 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -914407_6480482_4740872 -914426_6480521_4740081 type=superearth mass=2.8468729880125068 radius=1.2666108742092894 gravity=177 pressure=1600 tempK=129 oxygen=false locked=false rings=false rotation=53180 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -914407_6480482_4740872 -914619_6480479_4740425 type=ice mass=3.098920473393558 radius=1.3312106788774432 gravity=175 pressure=1600 tempK=141 oxygen=false locked=false rings=false rotation=43761 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 102729_-4691975_-1824860 102729_-4691975_-1824860 type=ice mass=1.900309120104137 radius=1.2514467561785527 gravity=121 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=6450 metallicity=0.6212021465115256 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1386765_1777561_3866474 1386765_1777561_3866474 type=barren mass=0.18218725160447546 radius=0.6469067641925179 gravity=44 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=10565 metallicity=0.8315665430971471 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1414282_6629949_2442943 1414282_6629949_2442943 type=barren mass=0.0028248942891845536 radius=0.2000303552532779 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=27236 metallicity=1.0977908061045079 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1932863_-2490869_1402243 1932863_-2490869_1402243 type=superearth mass=3.6830331610866343 radius=1.4336462116033544 gravity=179 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=15803 metallicity=1.3987381503775116 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2028640_-209750_5920429 type=icegiant mass=28.761040543123272 radius=3.8694011905491568 gravity=192 pressure=1600 tempK=89 oxygen=false locked=false rings=true rotation=9548 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2029022_-209735_5916898 type=gasgiant mass=254.61180685264645 radius=9.98653989270698 gravity=255 pressure=1600 tempK=70 oxygen=false locked=false rings=true rotation=7184 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030246_-209824_5920264 type=barren mass=0.004913333481680003 radius=0.22675389886512917 gravity=10 pressure=0 tempK=86 oxygen=false locked=false rings=false rotation=15500 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030430_-209815_5919472 type=ice mass=4.6864481443899955 radius=1.5652167800475636 gravity=191 pressure=1600 tempK=177 oxygen=false locked=false rings=false rotation=9268 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030438_-209803_5918826 type=icegiant mass=226.03501567053354 radius=9.482777391861841 gravity=251 pressure=1600 tempK=125 oxygen=false locked=false rings=true rotation=6807 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030578_-209804_5919889 type=greenhouse mass=8.230783879096062 radius=1.903584469464533 gravity=227 pressure=1600 tempK=310 oxygen=false locked=false rings=false rotation=46326 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030603_-209804_5920078 type=barren mass=0.0392839495614152 radius=0.40667920422127396 gravity=24 pressure=3 tempK=140 oxygen=false locked=false rings=false rotation=11624 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030688_-209810_5919897 type=superearth mass=17.775507536447584 radius=2.249378149861664 gravity=351 pressure=1600 tempK=851 oxygen=false locked=false rings=false rotation=24739 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030695_-209809_5919888 type=greenhouse mass=25.397641051281813 radius=2.4215629335179583 gravity=400 pressure=1600 tempK=855 oxygen=false locked=false rings=false rotation=44158 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030702_-209809_5919874 type=lava mass=4.776528115855772 radius=1.5061865406505028 gravity=211 pressure=44 tempK=1147 oxygen=false locked=true rings=false rotation=8495 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030705_-209809_5919877 type=lava mass=0.005404841629779387 radius=0.2632067849202486 gravity=8 pressure=0 tempK=861 oxygen=false locked=true rings=false rotation=23288 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2030702_-209809_5919874 2030736_-209808_5919916 type=greenhouse mass=3.2082270933212507 radius=1.3611153732714876 gravity=173 pressure=1059 tempK=424 oxygen=false locked=false rings=true rotation=10818 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2039398_8275570_9269915 2039398_8275570_9269915 type=ice mass=0.06958788439052067 radius=0.5235138597237201 gravity=25 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=9548 metallicity=1.4356709862180739 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2241385_8907861_-1975542 2241385_8907861_-1975542 type=ice mass=5.733141562078599 radius=1.6405990388349718 gravity=213 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=18439 metallicity=0.9981792538177909 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 370568_3515329_-2658033 370568_3515329_-2658033 type=ice mass=0.5620845235010506 radius=0.8124595416171261 gravity=85 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=13437 metallicity=0.45217268079600526 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4076707_1478856_6199021 4076254_1478945_6196302 type=barren mass=0.003007142527453317 radius=0.2192772368225473 gravity=6 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=8692 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4076707_1478856_6199021 4076349_1478856_6198826 type=icegiant mass=291.8884858342837 radius=10.597766739631862 gravity=260 pressure=1600 tempK=202 oxygen=false locked=false rings=true rotation=7338 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4076707_1478856_6199021 4076707_1478856_6199021 type=lava mass=1.3487688784875655 radius=1.021677182631749 gravity=129 pressure=0 tempK=4794 oxygen=false locked=true rings=false rotation=28070 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4076707_1478856_6199021 4076712_1478856_6199003 type=desert mass=2.660035614293332 radius=1.339988705282001 gravity=148 pressure=137 tempK=542 oxygen=false locked=false rings=false rotation=86895 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4076707_1478856_6199021 4076783_1478853_6199018 type=superearth mass=33.37291552120821 radius=2.462976278612129 gravity=400 pressure=1600 tempK=503 oxygen=false locked=false rings=false rotation=23809 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4076707_1478856_6199021 4078344_1478888_6198481 type=barren mass=0.01924329724609664 radius=0.3608682099209395 gravity=15 pressure=3 tempK=51 oxygen=false locked=false rings=false rotation=31393 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5171696_5557639_1152562 5171696_5557639_1152562 type=ice mass=19.82417253834241 radius=2.1183410616555247 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=19426 metallicity=0.6074607533120471 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5699530_6314857_-235309 5699530_6314857_-235309 type=superearth mass=5.126645332062038 radius=1.6736751477831422 gravity=183 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=18291 metallicity=0.7051798242602939 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5799956_-3218726_4876987 5799956_-3218726_4876987 type=ice mass=0.04806093411441581 radius=0.44010709889146316 gravity=25 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=19751 metallicity=1.1250319768630241 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6058862_2614790_2030893 6056728_2614916_2032616 type=ice mass=0.15272835548480254 radius=0.5796954533244889 gravity=45 pressure=190 tempK=45 oxygen=false locked=false rings=true rotation=19823 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6058862_2614790_2030893 6058410_2614864_2029239 type=icegiant mass=21.211291392018328 radius=3.389605866397197 gravity=185 pressure=1600 tempK=102 oxygen=false locked=false rings=true rotation=5442 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6058862_2614790_2030893 6058500_2614795_2030763 type=gasgiant mass=104.55020813243003 radius=6.781850976788818 gravity=227 pressure=1600 tempK=216 oxygen=false locked=false rings=true rotation=5441 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6058862_2614790_2030893 6058661_2614798_2030967 type=gasgiant mass=311.7431754492103 radius=10.905370864836637 gravity=262 pressure=1600 tempK=291 oxygen=false locked=false rings=false rotation=6196 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6058862_2614790_2030893 6058862_2614790_2030893 type=lava mass=29.975680424216204 radius=2.438270655680055 gravity=400 pressure=98 tempK=5672 oxygen=false locked=true rings=false rotation=50008 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6058862_2614790_2030893 6058886_2614790_2030901 type=barren mass=0.002408548470581327 radius=0.20651265210298247 gravity=6 pressure=0 tempK=432 oxygen=false locked=false rings=false rotation=16630 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6058862_2614790_2030893 6058957_2614792_2030875 type=ice mass=0.01574027225297693 radius=0.3373745430786347 gravity=14 pressure=0 tempK=181 oxygen=false locked=false rings=false rotation=20393 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6643221_-3045831_-3292025 6643221_-3045831_-3292025 type=barren mass=0.002889217083861708 radius=0.2071390155595182 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=60295 metallicity=0.43353637696850833 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7759431_-1367342_9293025 7759431_-1367342_9293025 type=ice mass=12.12677071434844 radius=2.0339633844371137 gravity=293 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=17385 metallicity=1.2409342000592618 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8695004_5647655_6839165 8695004_5647655_6839165 type=superearth mass=16.511656067843727 radius=2.1417234039988013 gravity=360 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=24555 metallicity=0.5487508545154615 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9350867_3953928_9075861 9350867_3953928_9075861 type=ice mass=0.9698264187238621 radius=0.9930186947160022 gravity=98 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=18465 metallicity=0.5426956430223249 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9450953_4739501_-3567012 9450953_4739501_-3567012 type=ice mass=7.087855370751103 radius=1.6547219994680025 gravity=259 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=6260 metallicity=0.4540346624894932 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1318696_8986598_-3915037 id=-905285817 kind=STAR name=PGS--5002361.5002361.-5002361 starTemp=100 starSize=1.042992115020752 + system -1353458_-2041995_3669964 id=-1793186305 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless + system -1504868_3859101_7238142 id=-731140245 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless + system -1702786_5224077_9097589 id=-1673402021 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless + system -2157794_-3988883_-1548825 id=-1640872829 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless + system -362968_4023206_-1712631 id=-429342865 kind=ROGUE_PLANET name=PGR--5002361.0.-5002361 starless + system -4110886_-2675466_7716656 id=-305214521 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless + system -4807070_1870953_3722184 id=-796796053 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless + system -914407_6480482_4740872 id=-1744473825 kind=STAR name=PGS--5002361.5002361.0 starTemp=40 starSize=0.9455409646034241 + system 102729_-4691975_-1824860 id=-986449737 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless + system 1386765_1777561_3866474 id=-1806957165 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 1414282_6629949_2442943 id=-1333921245 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless + system 1932863_-2490869_1402243 id=-681598609 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless + system 2030702_-209809_5919874 id=-129286269 kind=STAR name=PGS-0.-5002361.5002361 starTemp=40 starSize=0.6718353629112244 + system 2039398_8275570_9269915 id=-1830124441 kind=ROGUE_PLANET name=PGR-0.5002361.5002361 starless + system 2241385_8907861_-1975542 id=-704831901 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless + system 370568_3515329_-2658033 id=-1989327477 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless + system 4076707_1478856_6199021 id=-108865093 kind=STAR name=PGS-0.0.5002361 starTemp=150 starSize=1.3768268823623657 + system 5171696_5557639_1152562 id=-1586917565 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless + system 5699530_6314857_-235309 id=-1764830897 kind=ROGUE_PLANET name=PGR-5002361.5002361.-5002361 starless + system 5799956_-3218726_4876987 id=-780232045 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless + system 6058862_2614790_2030893 id=-1811230377 kind=STAR name=PGS-5002361.0.0 starTemp=150 starSize=1.5379765033721924 + system 6643221_-3045831_-3292025 id=-383094489 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless + system 7759431_-1367342_9293025 id=-20326109 kind=ROGUE_PLANET name=PGR-5002361.-5002361.5002361 starless + system 8695004_5647655_6839165 id=-1415522037 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless + system 9350867_3953928_9075861 id=-1168331549 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless + system 9450953_4739501_-3567012 id=-1579933465 kind=ROGUE_PLANET name=PGR-5002361.0.-5002361 starless +seed 6942069 systems=27 + body -2000631_2857747_461230 -2000631_2857747_461230 kind=ROGUE_PLANET orbit=0 radius=1.1175634439669084 starId=-205872669 frame=true + body -2314717_-1231317_1000094 -2314717_-1231317_1000094 kind=MOON orbit=0 radius=0.20000204191531482 starId=-901558001 frame=false + body -2314717_-1231317_1000094 -2314717_-1231317_1000094 kind=MOON orbit=0 radius=0.21789599586602817 starId=-901558001 frame=false + body -2314717_-1231317_1000094 -2314717_-1231317_1000094 kind=ROGUE_PLANET orbit=0 radius=1.863005654483607 starId=-901558001 frame=true + body -2877371_7494376_5224403 -2877371_7494376_5224403 kind=ROGUE_PLANET orbit=0 radius=1.2717789680091072 starId=-665523365 frame=true + body -2895205_-1927811_6875549 -2895205_-1927811_6875549 kind=ROGUE_PLANET orbit=0 radius=2.3163732961564074 starId=-909247865 frame=true + body -4275006_509094_-3579362 -4262355_509628_-3582487 kind=ASTEROID_BELT orbit=69744 radius=0.0 starId=-1541462001 frame=true + body -4275006_509094_-3579362 -4268441_508768_-3574542 kind=PLANET orbit=43590 radius=0.8647633663246215 starId=-1541462001 frame=true + body -4275006_509094_-3579362 -4274966_509095_-3579392 kind=PLANET orbit=267 radius=0.737582062833023 starId=-1541462001 frame=true + body -4275006_509094_-3579362 -4274993_509128_-3578183 kind=ASTEROID_BELT orbit=6308 radius=0.0 starId=-1541462001 frame=true + body -4275006_509094_-3579362 -4275006_509094_-3579362 kind=STAR orbit=0 radius=0.0 starId=-1541462001 frame=true + body -4275006_509094_-3579362 -4275120_509094_-3579130 kind=STAR orbit=1380 radius=82.15348955512047 starId=-1541462002 frame=true + body -4275006_509094_-3579362 -4275443_509018_-3582785 kind=MOON orbit=18458 radius=0.2802839163886913 starId=-1541462001 frame=false + body -4275006_509094_-3579362 -4275443_509018_-3582785 kind=PLANET orbit=18458 radius=1.5665193433650293 starId=-1541462001 frame=true + body -4275006_509094_-3579362 -4275958_509131_-3579524 kind=PLANET orbit=5167 radius=2.212651053453857 starId=-1541462001 frame=true + body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=GAS_GIANT orbit=11356 radius=6.011853442836375 starId=-1541462001 frame=true + body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=MOON orbit=11356 radius=0.49926944261156775 starId=-1541462001 frame=false + body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=MOON orbit=11356 radius=0.6192696527926773 starId=-1541462001 frame=false + body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=MOON orbit=11356 radius=0.6743314451130911 starId=-1541462001 frame=false + body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=MOON orbit=11356 radius=0.7322653156046546 starId=-1541462001 frame=false + body -4438016_6980803_4442099 -4437971_6980802_4441977 kind=GAS_GIANT orbit=697 radius=5.847417597949033 starId=-1046829965 frame=true + body -4438016_6980803_4442099 -4438012_6980803_4442118 kind=PLANET orbit=103 radius=1.6973042599502073 starId=-1046829965 frame=true + body -4438016_6980803_4442099 -4438016_6980803_4442099 kind=STAR orbit=0 radius=0.0 starId=-1046829965 frame=true + body -4438016_6980803_4442099 -4438019_6980803_4442098 kind=PLANET orbit=16 radius=1.7455655341946663 starId=-1046829965 frame=true + body -4438016_6980803_4442099 -4438081_6980802_4442067 kind=ASTEROID_BELT orbit=387 radius=0.0 starId=-1046829965 frame=true + body -4438016_6980803_4442099 -4438219_6980809_4442053 kind=ASTEROID_BELT orbit=1115 radius=0.0 starId=-1046829965 frame=true + body -454681_4814105_7133477 -454681_4814105_7133477 kind=MOON orbit=0 radius=0.8248445313980841 starId=-331820225 frame=false + body -454681_4814105_7133477 -454681_4814105_7133477 kind=ROGUE_PLANET orbit=0 radius=0.6183832792523067 starId=-331820225 frame=true + body -4597348_-2289120_-3893104 -4597348_-2289120_-3893104 kind=ROGUE_PLANET orbit=0 radius=0.8002391142960747 starId=-45556669 frame=true + body -4680832_8971816_-4852244 -4680832_8971816_-4852244 kind=MOON orbit=0 radius=0.8726694211157258 starId=-800100513 frame=false + body -4680832_8971816_-4852244 -4680832_8971816_-4852244 kind=MOON orbit=0 radius=1.8437515181150548 starId=-800100513 frame=false + body -4680832_8971816_-4852244 -4680832_8971816_-4852244 kind=ROGUE_PLANET orbit=0 radius=0.24455038227961948 starId=-800100513 frame=true + body 1167088_-1577312_6736547 1167088_-1577312_6736547 kind=MOON orbit=0 radius=2.4810088471172254 starId=-41953553 frame=false + body 1167088_-1577312_6736547 1167088_-1577312_6736547 kind=ROGUE_PLANET orbit=0 radius=2.308617529166714 starId=-41953553 frame=true + body 1436889_2367423_-1041444 1436889_2367423_-1041444 kind=ROGUE_PLANET orbit=0 radius=0.4647793875769384 starId=-718399965 frame=true + body 1474783_6118657_-2639989 1474783_6118657_-2639989 kind=ROGUE_PLANET orbit=0 radius=0.21285660343752053 starId=-27450993 frame=true + body 167759_-751993_-3497839 167759_-751993_-3497839 kind=MOON orbit=0 radius=1.653297304016185 starId=-771118101 frame=false + body 167759_-751993_-3497839 167759_-751993_-3497839 kind=ROGUE_PLANET orbit=0 radius=1.7397452680925143 starId=-771118101 frame=true + body 441136_1799137_1921288 441136_1799137_1921288 kind=MOON orbit=0 radius=7.563308321550477 starId=-1525225641 frame=false + body 441136_1799137_1921288 441136_1799137_1921288 kind=ROGUE_PLANET orbit=0 radius=0.20117409820683874 starId=-1525225641 frame=true + body 4829162_6045847_8018026 4829126_6045849_8018058 kind=ASTEROID_BELT orbit=260 radius=0.0 starId=-51688385 frame=true + body 4829162_6045847_8018026 4829158_6045847_8018023 kind=PLANET orbit=24 radius=2.350205838409168 starId=-51688385 frame=true + body 4829162_6045847_8018026 4829160_6045847_8018021 kind=ASTEROID_BELT orbit=31 radius=0.0 starId=-51688385 frame=true + body 4829162_6045847_8018026 4829161_6045847_8018016 kind=GAS_GIANT orbit=56 radius=3.0667534125078753 starId=-51688385 frame=true + body 4829162_6045847_8018026 4829162_6045847_8018026 kind=STAR orbit=0 radius=0.0 starId=-51688385 frame=true + body 4829162_6045847_8018026 4829163_6045847_8018027 kind=PLANET orbit=8 radius=0.6618193175012526 starId=-51688385 frame=true + body 4829162_6045847_8018026 4829180_6045847_8018001 kind=MOON orbit=163 radius=0.20488986961295125 starId=-51688385 frame=false + body 4829162_6045847_8018026 4829180_6045847_8018001 kind=MOON orbit=163 radius=0.24662987357862132 starId=-51688385 frame=false + body 4829162_6045847_8018026 4829180_6045847_8018001 kind=PLANET orbit=163 radius=0.48927571213765514 starId=-51688385 frame=true + body 4829162_6045847_8018026 4829282_6045847_8018043 kind=STAR orbit=650 radius=85.98566706061364 starId=-51688386 frame=true + body 4853201_4468647_7791279 4853201_4468647_7791279 kind=MOON orbit=0 radius=2.338014275612157 starId=-1963232729 frame=false + body 4853201_4468647_7791279 4853201_4468647_7791279 kind=ROGUE_PLANET orbit=0 radius=0.4239783392591294 starId=-1963232729 frame=true + body 5345275_3039615_-4867366 5345275_3039615_-4867366 kind=ROGUE_PLANET orbit=0 radius=0.32781097335099096 starId=-1930757837 frame=true + body 5791350_-2733430_-4256275 5791350_-2733430_-4256275 kind=ROGUE_PLANET orbit=0 radius=0.21174735932880662 starId=-220927317 frame=true + body 587917_5522727_989590 587829_5522722_989492 kind=ASTEROID_BELT orbit=705 radius=0.0 starId=-1767151601 frame=true + body 587917_5522727_989590 587859_5522730_989564 kind=PLANET orbit=338 radius=1.928779571339509 starId=-1767151601 frame=true + body 587917_5522727_989590 587863_5522723_989652 kind=GAS_GIANT orbit=441 radius=9.769767771696792 starId=-1767151601 frame=true + body 587917_5522727_989590 587863_5522723_989652 kind=MOON orbit=441 radius=0.2174686697377889 starId=-1767151601 frame=false + body 587917_5522727_989590 587863_5522723_989652 kind=MOON orbit=441 radius=0.515587549611561 starId=-1767151601 frame=false + body 587917_5522727_989590 587905_5522727_989595 kind=MOON orbit=71 radius=0.3394053532009783 starId=-1767151601 frame=false + body 587917_5522727_989590 587905_5522727_989595 kind=MOON orbit=71 radius=0.7446296027401826 starId=-1767151601 frame=false + body 587917_5522727_989590 587905_5522727_989595 kind=PLANET orbit=71 radius=0.3658287705174149 starId=-1767151601 frame=true + body 587917_5522727_989590 587910_5522727_989607 kind=GAS_GIANT orbit=100 radius=8.196656972278635 starId=-1767151601 frame=true + body 587917_5522727_989590 587910_5522727_989607 kind=MOON orbit=100 radius=0.22159543085238867 starId=-1767151601 frame=false + body 587917_5522727_989590 587917_5522727_989588 kind=MOON orbit=11 radius=0.22325450226540466 starId=-1767151601 frame=false + body 587917_5522727_989590 587917_5522727_989588 kind=PLANET orbit=11 radius=2.257547577904001 starId=-1767151601 frame=true + body 587917_5522727_989590 587917_5522727_989590 kind=STAR orbit=0 radius=0.0 starId=-1767151601 frame=true + body 587917_5522727_989590 587918_5522727_989590 kind=MOON orbit=6 radius=0.2586921589715278 starId=-1767151601 frame=false + body 587917_5522727_989590 587918_5522727_989590 kind=PLANET orbit=6 radius=0.788616566373262 starId=-1767151601 frame=true + body 587917_5522727_989590 587921_5522727_989589 kind=PLANET orbit=20 radius=1.6748013792552296 starId=-1767151601 frame=true + body 587917_5522727_989590 587923_5522727_989581 kind=ASTEROID_BELT orbit=55 radius=0.0 starId=-1767151601 frame=true + body 587917_5522727_989590 587923_5522727_989594 kind=PLANET orbit=40 radius=1.4511788031234742 starId=-1767151601 frame=true + body 587917_5522727_989590 587947_5522728_989592 kind=GAS_GIANT orbit=160 radius=7.300658322926931 starId=-1767151601 frame=true + body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.2067364242405062 starId=-1767151601 frame=false + body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.23017884841460212 starId=-1767151601 frame=false + body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.2948552526319255 starId=-1767151601 frame=false + body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.3089593434746649 starId=-1767151601 frame=false + body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.3592246315698835 starId=-1767151601 frame=false + body 7060560_5700377_2876755 7060560_5700377_2876755 kind=ROGUE_PLANET orbit=0 radius=0.2718294017783941 starId=-1316506565 frame=true + body 7136101_9473497_8932593 7136101_9473497_8932593 kind=ROGUE_PLANET orbit=0 radius=0.5764923072666365 starId=-723847149 frame=true + body 7364543_-1483056_2565191 7364543_-1483056_2565191 kind=MOON orbit=0 radius=0.2429303883510497 starId=-1945655921 frame=false + body 7364543_-1483056_2565191 7364543_-1483056_2565191 kind=MOON orbit=0 radius=0.8234012503125645 starId=-1945655921 frame=false + body 7364543_-1483056_2565191 7364543_-1483056_2565191 kind=ROGUE_PLANET orbit=0 radius=1.815800241268268 starId=-1945655921 frame=true + body 7828323_3394113_830032 7828323_3394113_830032 kind=MOON orbit=0 radius=2.1984708188976825 starId=-784249741 frame=false + body 7828323_3394113_830032 7828323_3394113_830032 kind=ROGUE_PLANET orbit=0 radius=1.6484804785844318 starId=-784249741 frame=true + body 7974653_6760692_-2065905 7974653_6760692_-2065905 kind=ROGUE_PLANET orbit=0 radius=1.9935622970096312 starId=-1716020649 frame=true + body 7996706_-2186877_7355245 7969279_-2186877_7337231 kind=STAR orbit=175478 radius=92.08370618999004 starId=-1661134222 frame=true + body 7996706_-2186877_7355245 7996538_-2186870_7355360 kind=GAS_GIANT orbit=1090 radius=5.784037934580867 starId=-1661134221 frame=true + body 7996706_-2186877_7355245 7996596_-2186880_7355218 kind=ASTEROID_BELT orbit=605 radius=0.0 starId=-1661134221 frame=true + body 7996706_-2186877_7355245 7996683_-2186878_7355223 kind=MOON orbit=170 radius=0.2437557445272609 starId=-1661134221 frame=false + body 7996706_-2186877_7355245 7996683_-2186878_7355223 kind=MOON orbit=170 radius=0.6245520811489388 starId=-1661134221 frame=false + body 7996706_-2186877_7355245 7996683_-2186878_7355223 kind=PLANET orbit=170 radius=1.2202907808972563 starId=-1661134221 frame=true + body 7996706_-2186877_7355245 7996704_-2186877_7355240 kind=STAR orbit=28 radius=92.08370618999004 starId=-1661134223 frame=true + body 7996706_-2186877_7355245 7996706_-2186877_7355245 kind=STAR orbit=0 radius=0.0 starId=-1661134221 frame=true + body 7996706_-2186877_7355245 7996851_-2186877_7355537 kind=ASTEROID_BELT orbit=1744 radius=0.0 starId=-1661134221 frame=true + body 8321778_4704939_9248429 8321469_4704913_9249045 kind=MOON orbit=3687 radius=0.5305367936750038 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8321469_4704913_9249045 kind=MOON orbit=3687 radius=0.7109215379700333 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8321469_4704913_9249045 kind=PLANET orbit=3687 radius=0.45245851529142395 starId=-1361770949 frame=true + body 8321778_4704939_9248429 8321730_4704942_9248455 kind=ASTEROID_BELT orbit=292 radius=0.0 starId=-1361770949 frame=true + body 8321778_4704939_9248429 8321731_4704936_9248343 kind=GAS_GIANT orbit=526 radius=10.158618618611886 starId=-1361770949 frame=true + body 8321778_4704939_9248429 8321731_4704936_9248343 kind=MOON orbit=526 radius=0.21078927632064628 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8321731_4704936_9248343 kind=MOON orbit=526 radius=0.3533663064113992 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8321731_4704936_9248343 kind=MOON orbit=526 radius=0.39397170175713786 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8321731_4704936_9248343 kind=MOON orbit=526 radius=0.45392051937072936 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8321778_4704939_9248429 kind=STAR orbit=0 radius=0.0 starId=-1361770949 frame=true + body 8321778_4704939_9248429 8321781_4704939_9248425 kind=PLANET orbit=28 radius=1.2131860370084657 starId=-1361770949 frame=true + body 8321778_4704939_9248429 8321787_4704939_9248459 kind=STAR orbit=169 radius=76.91228431642055 starId=-1361770950 frame=true + body 8321778_4704939_9248429 8321848_4704948_9248254 kind=GAS_GIANT orbit=1012 radius=7.297168858591538 starId=-1361770949 frame=true + body 8321778_4704939_9248429 8321848_4704948_9248254 kind=MOON orbit=1012 radius=0.21183434315225672 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8321848_4704948_9248254 kind=MOON orbit=1012 radius=0.3052065627341558 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8321848_4704948_9248254 kind=MOON orbit=1012 radius=0.4885069925715317 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8321848_4704948_9248254 kind=MOON orbit=1012 radius=0.7013628705572932 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8322209_4704920_9248345 kind=MOON orbit=2350 radius=0.39830164316357086 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8322209_4704920_9248345 kind=MOON orbit=2350 radius=0.5521362719668897 starId=-1361770949 frame=false + body 8321778_4704939_9248429 8322209_4704920_9248345 kind=PLANET orbit=2350 radius=2.278982012939558 starId=-1361770949 frame=true + body 8321778_4704939_9248429 8322380_4704929_9249353 kind=ASTEROID_BELT orbit=5899 radius=0.0 starId=-1361770949 frame=true + body 958466_-2560139_3404636 958466_-2560139_3404636 kind=ROGUE_PLANET orbit=0 radius=0.4161581934278241 starId=-680806101 frame=true + derived -2000631_2857747_461230 -2000631_2857747_461230 type=ice mass=1.5509619296421744 radius=1.1175634439669084 gravity=124 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=47972 metallicity=0.577565904469171 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2314717_-1231317_1000094 -2314717_-1231317_1000094 type=superearth mass=7.692509079840432 radius=1.863005654483607 gravity=222 pressure=0 tempK=43 oxygen=false locked=false rings=false rotation=11824 metallicity=0.7141346578312251 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2877371_7494376_5224403 -2877371_7494376_5224403 type=ice mass=2.691093337632512 radius=1.2717789680091072 gravity=166 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=7094 metallicity=0.581091632464666 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2895205_-1927811_6875549 -2895205_-1927811_6875549 type=superearth mass=19.592550392607542 radius=2.3163732961564074 gravity=365 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=27177 metallicity=1.4608463176547102 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4275006_509094_-3579362 -4262355_509628_-3582487 type=barren mass=0.006232896285339044 radius=0.2619277258550008 gravity=9 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=63382 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4275006_509094_-3579362 -4268441_508768_-3574542 type=ice mass=0.6003813645154001 radius=0.8647633663246215 gravity=80 pressure=1600 tempK=80 oxygen=false locked=false rings=false rotation=10347 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4275006_509094_-3579362 -4274966_509095_-3579392 type=desert mass=0.3928649259378479 radius=0.737582062833023 gravity=72 pressure=1 tempK=528 oxygen=false locked=false rings=false rotation=11734 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4275006_509094_-3579362 -4274993_509128_-3578183 type=icegiant mass=94.73366886910857 radius=6.4972655072397725 gravity=224 pressure=1600 tempK=225 oxygen=false locked=false rings=false rotation=5925 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4275006_509094_-3579362 -4275006_509094_-3579362 type=unclassified mass=0.006157838693660983 radius=0.24531213715311925 gravity=10 pressure=0 tempK=8640 oxygen=false locked=true rings=true rotation=9756 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4275006_509094_-3579362 -4275120_509094_-3579130 type=superearth mass=30.459553437561752 radius=2.379810241823191 gravity=400 pressure=1600 tempK=523 oxygen=false locked=false rings=false rotation=30850 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4275006_509094_-3579362 -4275443_509018_-3582785 type=superearth mass=3.952703328435687 radius=1.5665193433650293 gravity=161 pressure=1600 tempK=143 oxygen=false locked=false rings=false rotation=15199 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4275006_509094_-3579362 -4275958_509131_-3579524 type=superearth mass=16.210601325628947 radius=2.212651053453857 gravity=331 pressure=1600 tempK=270 oxygen=false locked=false rings=false rotation=8326 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4275006_509094_-3579362 -4276233_509117_-3581095 type=icegiant mass=79.23977417178959 radius=6.011853442836375 gravity=219 pressure=1600 tempK=167 oxygen=false locked=false rings=true rotation=13874 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4438016_6980803_4442099 -4437971_6980802_4441977 type=gasgiant mass=74.34322522188954 radius=5.847417597949033 gravity=217 pressure=1600 tempK=132 oxygen=false locked=false rings=false rotation=5180 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4438016_6980803_4442099 -4438012_6980803_4442118 type=greenhouse mass=5.634910161229004 radius=1.6973042599502073 gravity=196 pressure=1600 tempK=289 oxygen=false locked=false rings=false rotation=11508 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4438016_6980803_4442099 -4438016_6980803_4442099 type=lava mass=0.06323371623274694 radius=0.473488411952418 gravity=28 pressure=0 tempK=1796 oxygen=false locked=true rings=false rotation=52867 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4438016_6980803_4442099 -4438019_6980803_4442098 type=lava mass=6.6904109023411795 radius=1.7455655341946663 gravity=220 pressure=1600 tempK=1010 oxygen=false locked=true rings=false rotation=20793 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4438016_6980803_4442099 -4438081_6980802_4442067 type=icegiant mass=241.47156099019836 radius=9.759095883804656 gravity=254 pressure=1600 tempK=177 oxygen=false locked=false rings=true rotation=6705 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4438016_6980803_4442099 -4438219_6980809_4442053 type=barren mass=0.0031404818084259196 radius=0.20917351228651077 gravity=7 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=19110 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -454681_4814105_7133477 -454681_4814105_7133477 type=ice mass=0.17426138830929258 radius=0.6183832792523067 gravity=46 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=43883 metallicity=0.4230696455081826 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4597348_-2289120_-3893104 -4597348_-2289120_-3893104 type=barren mass=0.4565249679975668 radius=0.8002391142960747 gravity=71 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=8054 metallicity=0.4627221934143715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4680832_8971816_-4852244 -4680832_8971816_-4852244 type=barren mass=0.0062750825875540935 radius=0.24455038227961948 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=32229 metallicity=0.980274814369234 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1167088_-1577312_6736547 1167088_-1577312_6736547 type=superearth mass=24.21660930929242 radius=2.308617529166714 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=78771 metallicity=0.9850103985780603 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1436889_2367423_-1041444 1436889_2367423_-1041444 type=barren mass=0.04740156517764068 radius=0.4647793875769384 gravity=22 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=43740 metallicity=0.9490739923459447 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1474783_6118657_-2639989 1474783_6118657_-2639989 type=ice mass=0.0029047379942924965 radius=0.21285660343752053 gravity=6 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=44596 metallicity=0.40640924213177304 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 167759_-751993_-3497839 167759_-751993_-3497839 type=superearth mass=9.343148962232025 radius=1.7397452680925143 gravity=309 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=36699 metallicity=0.5144347213851472 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 441136_1799137_1921288 441136_1799137_1921288 type=barren mass=0.0022266009259117106 radius=0.20117409820683874 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=29510 metallicity=0.3831595348763942 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4829162_6045847_8018026 4829126_6045849_8018058 type=ice mass=2.1029791267430484 radius=1.2481868701659535 gravity=135 pressure=1600 tempK=113 oxygen=false locked=false rings=false rotation=17414 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4829162_6045847_8018026 4829158_6045847_8018023 type=superearth mass=22.189971166285517 radius=2.350205838409168 gravity=400 pressure=1600 tempK=417 oxygen=false locked=true rings=false rotation=9760 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4829162_6045847_8018026 4829160_6045847_8018021 type=exotic mass=1.0231194073980834 radius=1.0063670398132052 gravity=101 pressure=808 tempK=309 oxygen=false locked=true rings=false rotation=9403 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4829162_6045847_8018026 4829161_6045847_8018016 type=gasgiant mass=16.84943395728299 radius=3.0667534125078753 gravity=179 pressure=1600 tempK=251 oxygen=false locked=false rings=true rotation=9958 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4829162_6045847_8018026 4829162_6045847_8018026 type=barren mass=0.2022866885608661 radius=0.6454013420135354 gravity=49 pressure=0 tempK=961 oxygen=false locked=true rings=false rotation=32184 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4829162_6045847_8018026 4829163_6045847_8018027 type=barren mass=0.2682804120014818 radius=0.6618193175012526 gravity=61 pressure=12 tempK=340 oxygen=false locked=true rings=false rotation=90507 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4829162_6045847_8018026 4829180_6045847_8018001 type=ice mass=0.07539902914771969 radius=0.48927571213765514 gravity=31 pressure=54 tempK=62 oxygen=false locked=false rings=false rotation=94182 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4829162_6045847_8018026 4829282_6045847_8018043 type=gasgiant mass=79.50637799485442 radius=6.020639444361429 gravity=219 pressure=1600 tempK=81 oxygen=false locked=false rings=true rotation=9564 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4853201_4468647_7791279 4853201_4468647_7791279 type=ice mass=0.039769133653525045 radius=0.4239783392591294 gravity=22 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=16003 metallicity=0.8985853797134048 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5345275_3039615_-4867366 5345275_3039615_-4867366 type=barren mass=0.015090364122544584 radius=0.32781097335099096 gravity=14 pressure=0 tempK=21 oxygen=false locked=false rings=true rotation=9560 metallicity=0.6592137837908945 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5791350_-2733430_-4256275 5791350_-2733430_-4256275 type=barren mass=0.00356415955964074 radius=0.21174735932880662 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=true rotation=26620 metallicity=0.7832763078453421 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587829_5522722_989492 type=barren mass=0.03176468238245332 radius=0.37276832630008583 gravity=23 pressure=9 tempK=38 oxygen=false locked=false rings=false rotation=71896 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587859_5522730_989564 type=superearth mass=9.13029145017862 radius=1.928779571339509 gravity=245 pressure=1600 tempK=118 oxygen=false locked=false rings=false rotation=39692 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587863_5522723_989652 type=icegiant mass=242.0793237896986 radius=9.769767771696792 gravity=254 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=6186 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587905_5522727_989595 type=ice mass=0.02260281926731236 radius=0.3658287705174149 gravity=17 pressure=1 tempK=100 oxygen=false locked=false rings=false rotation=81831 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587910_5522727_989607 type=gasgiant mass=161.6547584471927 radius=8.196656972278635 gravity=241 pressure=1600 tempK=200 oxygen=false locked=false rings=true rotation=13770 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587917_5522727_989588 type=greenhouse mass=24.61686910908349 radius=2.257547577904001 gravity=400 pressure=1600 tempK=508 oxygen=false locked=true rings=false rotation=8592 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587917_5522727_989590 type=lava mass=23.773968929357295 radius=2.2377199011218356 gravity=400 pressure=1297 tempK=2204 oxygen=false locked=true rings=false rotation=8466 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587918_5522727_989590 type=desert mass=0.4919682647497833 radius=0.788616566373262 gravity=79 pressure=10 tempK=395 oxygen=false locked=true rings=false rotation=12150 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587921_5522727_989589 type=greenhouse mass=6.746705939385756 radius=1.6748013792552296 gravity=241 pressure=1600 tempK=377 oxygen=false locked=true rings=false rotation=39618 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587923_5522727_989581 type=superearth mass=3.982003394899363 radius=1.5415096258089278 gravity=168 pressure=1600 tempK=294 oxygen=false locked=false rings=false rotation=19367 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587923_5522727_989594 type=exotic mass=3.1161263559236354 radius=1.4511788031234742 gravity=148 pressure=1600 tempK=344 oxygen=false locked=true rings=false rotation=17339 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 587917_5522727_989590 587947_5522728_989592 type=gasgiant mass=123.86727922451554 radius=7.300658322926931 gravity=232 pressure=1600 tempK=158 oxygen=false locked=false rings=true rotation=6307 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7060560_5700377_2876755 7060560_5700377_2876755 type=barren mass=0.0075290595549173354 radius=0.2718294017783941 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=64530 metallicity=1.0669737181725631 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7136101_9473497_8932593 7136101_9473497_8932593 type=ice mass=0.16003853224505066 radius=0.5764923072666365 gravity=48 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=36716 metallicity=0.9058761263463274 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7364543_-1483056_2565191 7364543_-1483056_2565191 type=ice mass=8.68456268430672 radius=1.815800241268268 gravity=263 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=84169 metallicity=1.451623064986201 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7828323_3394113_830032 7828323_3394113_830032 type=superearth mass=5.581741802538662 radius=1.6484804785844318 gravity=205 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=12703 metallicity=0.9653087518188345 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7974653_6760692_-2065905 7974653_6760692_-2065905 type=ice mass=12.126402985250099 radius=1.9935622970096312 gravity=305 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=7264 metallicity=0.5593650899132638 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7996706_-2186877_7355245 7969279_-2186877_7337231 type=icegiant mass=66.51326254565343 radius=5.5712121763523825 gravity=214 pressure=1600 tempK=8 oxygen=false locked=false rings=true rotation=6163 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7996706_-2186877_7355245 7996538_-2186870_7355360 type=gasgiant mass=72.50292856904187 radius=5.784037934580867 gravity=217 pressure=1600 tempK=105 oxygen=false locked=false rings=false rotation=6439 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7996706_-2186877_7355245 7996596_-2186880_7355218 type=gasgiant mass=20.021182674614654 radius=3.3055672195119294 gravity=183 pressure=1600 tempK=141 oxygen=false locked=false rings=false rotation=10316 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7996706_-2186877_7355245 7996683_-2186878_7355223 type=superearth mass=2.556340141638674 radius=1.2202907808972563 gravity=172 pressure=1600 tempK=289 oxygen=false locked=false rings=false rotation=9626 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7996706_-2186877_7355245 7996704_-2186877_7355240 type=superearth mass=6.2774689588736585 radius=1.55198833221646 gravity=261 pressure=1600 tempK=617 oxygen=false locked=true rings=false rotation=6209 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7996706_-2186877_7355245 7996706_-2186877_7355245 type=barren mass=0.07531318336790613 radius=0.5091277160105288 gravity=29 pressure=0 tempK=998 oxygen=false locked=true rings=false rotation=77118 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7996706_-2186877_7355245 7996851_-2186877_7355537 type=gasgiant mass=85.27436538634228 radius=6.206792068644254 gravity=221 pressure=1600 tempK=83 oxygen=false locked=false rings=false rotation=8495 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8321778_4704939_9248429 8321469_4704913_9249045 type=barren mass=0.04124148346273014 radius=0.45245851529142395 gravity=20 pressure=18 tempK=49 oxygen=false locked=false rings=false rotation=15807 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8321778_4704939_9248429 8321730_4704942_9248455 type=ice mass=0.15832531713214607 radius=0.5991106795837686 gravity=44 pressure=26 tempK=143 oxygen=false locked=false rings=false rotation=6147 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8321778_4704939_9248429 8321731_4704936_9248343 type=gasgiant mass=264.81565349914257 radius=10.158618618611886 gravity=257 pressure=1600 tempK=253 oxygen=false locked=false rings=true rotation=6413 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8321778_4704939_9248429 8321778_4704939_9248429 type=lava mass=0.05077807073042751 radius=0.4307541898393471 gravity=27 pressure=0 tempK=2991 oxygen=false locked=true rings=false rotation=18596 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8321778_4704939_9248429 8321781_4704939_9248425 type=desert mass=1.7610003129621679 radius=1.2131860370084657 gravity=120 pressure=50 tempK=530 oxygen=false locked=true rings=false rotation=82167 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8321778_4704939_9248429 8321787_4704939_9248459 type=greenhouse mass=22.321865746529177 radius=2.376303730221804 gravity=395 pressure=1600 tempK=376 oxygen=false locked=false rings=false rotation=14594 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8321778_4704939_9248429 8321848_4704948_9248254 type=gasgiant mass=123.73115160984537 radius=7.297168858591538 gravity=232 pressure=1600 tempK=183 oxygen=false locked=false rings=false rotation=6866 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8321778_4704939_9248429 8322209_4704920_9248345 type=superearth mass=16.336557863330242 radius=2.278982012939558 gravity=315 pressure=1600 tempK=130 oxygen=false locked=false rings=false rotation=20488 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8321778_4704939_9248429 8322380_4704929_9249353 type=ice mass=13.082720853039595 radius=2.0303380169775687 gravity=317 pressure=1600 tempK=71 oxygen=false locked=false rings=false rotation=76471 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 958466_-2560139_3404636 958466_-2560139_3404636 type=barren mass=0.03893878624401527 radius=0.4161581934278241 gravity=22 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=34432 metallicity=0.6902859630758016 terrain=TerrainOption[NATIVE genType=0 w=1] + system -2000631_2857747_461230 id=-205872669 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless + system -2314717_-1231317_1000094 id=-901558001 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless + system -2877371_7494376_5224403 id=-665523365 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless + system -2895205_-1927811_6875549 id=-909247865 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless + system -4275006_509094_-3579362 id=-1541462001 kind=STAR name=PGS--5002361.0.-5002361 starTemp=220 starSize=2.3564164638519287 + system -4438016_6980803_4442099 id=-1046829965 kind=STAR name=PGS--5002361.5002361.0 starTemp=70 starSize=0.8875247836112976 + system -454681_4814105_7133477 id=-331820225 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless + system -4597348_-2289120_-3893104 id=-45556669 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless + system -4680832_8971816_-4852244 id=-800100513 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless + system 1167088_-1577312_6736547 id=-41953553 kind=ROGUE_PLANET name=PGR-0.-5002361.5002361 starless + system 1436889_2367423_-1041444 id=-718399965 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless + system 1474783_6118657_-2639989 id=-27450993 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless + system 167759_-751993_-3497839 id=-771118101 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless + system 441136_1799137_1921288 id=-1525225641 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 4829162_6045847_8018026 id=-51688385 kind=STAR name=PGS-0.5002361.5002361 starTemp=40 starSize=0.7876309156417847 + system 4853201_4468647_7791279 id=-1963232729 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless + system 5345275_3039615_-4867366 id=-1930757837 kind=ROGUE_PLANET name=PGR-5002361.0.-5002361 starless + system 5791350_-2733430_-4256275 id=-220927317 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless + system 587917_5522727_989590 id=-1767151601 kind=STAR name=PGS-0.5002361.0 starTemp=40 starSize=0.8977140188217163 + system 7060560_5700377_2876755 id=-1316506565 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless + system 7136101_9473497_8932593 id=-723847149 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless + system 7364543_-1483056_2565191 id=-1945655921 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless + system 7828323_3394113_830032 id=-784249741 kind=ROGUE_PLANET name=PGR-5002361.0.0 starless + system 7974653_6760692_-2065905 id=-1716020649 kind=ROGUE_PLANET name=PGR-5002361.5002361.-5002361 starless + system 7996706_-2186877_7355245 id=-1661134221 kind=STAR name=PGS-5002361.-5002361.5002361 starTemp=40 starSize=0.8434891104698181 + system 8321778_4704939_9248429 id=-1361770949 kind=STAR name=PGS-5002361.0.5002361 starTemp=100 starSize=1.2055790424346924 + system 958466_-2560139_3404636 id=-680806101 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless +seed 2147483647 systems=27 + body -1113821_-4579746_-1376000 -1113821_-4579746_-1376000 kind=MOON orbit=0 radius=0.2350425970115344 starId=-237253153 frame=false + body -1113821_-4579746_-1376000 -1113821_-4579746_-1376000 kind=MOON orbit=0 radius=0.391775468106053 starId=-237253153 frame=false + body -1113821_-4579746_-1376000 -1113821_-4579746_-1376000 kind=ROGUE_PLANET orbit=0 radius=1.2451548691334666 starId=-237253153 frame=true + body -1135507_2621614_-4213570 -1135434_2621617_-4213557 kind=MOON orbit=396 radius=0.3930718179120912 starId=-1468730469 frame=false + body -1135507_2621614_-4213570 -1135434_2621617_-4213557 kind=PLANET orbit=396 radius=0.2053908857293373 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1135440_2621614_-4213505 kind=ASTEROID_BELT orbit=497 radius=0.0 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1135484_2621613_-4213564 kind=MOON orbit=126 radius=0.3493600954378102 starId=-1468730469 frame=false + body -1135507_2621614_-4213570 -1135484_2621613_-4213564 kind=PLANET orbit=126 radius=2.3681492037108587 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=GAS_GIANT orbit=895 radius=8.346217680560606 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=MOON orbit=895 radius=0.21990251326387936 starId=-1468730469 frame=false + body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=MOON orbit=895 radius=0.2248619222244948 starId=-1468730469 frame=false + body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=MOON orbit=895 radius=0.23270214640355974 starId=-1468730469 frame=false + body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=MOON orbit=895 radius=0.5150317879288896 starId=-1468730469 frame=false + body -1135507_2621614_-4213570 -1135498_2621613_-4213585 kind=MOON orbit=91 radius=0.24832120239863054 starId=-1468730469 frame=false + body -1135507_2621614_-4213570 -1135498_2621613_-4213585 kind=PLANET orbit=91 radius=0.2004764575690401 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1135505_2621614_-4213571 kind=PLANET orbit=13 radius=1.124285627698082 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1135507_2621614_-4213570 kind=STAR orbit=0 radius=0.0 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1135508_2621614_-4213565 kind=MOON orbit=27 radius=0.5793185118301637 starId=-1468730469 frame=false + body -1135507_2621614_-4213570 -1135508_2621614_-4213565 kind=PLANET orbit=27 radius=0.20468922867125786 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1135586_2621624_-4213820 kind=PLANET orbit=1402 radius=2.023111892962628 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1135838_2621597_-4213313 kind=ASTEROID_BELT orbit=2243 radius=0.0 starId=-1468730469 frame=true + body -1135507_2621614_-4213570 -1137206_2621614_-4193101 kind=STAR orbit=109836 radius=107.86106354176998 starId=-1468730470 frame=true + body -3348450_-3083316_687546 -3348450_-3083316_687546 kind=ROGUE_PLANET orbit=0 radius=0.9166965049835585 starId=-1251094673 frame=true + body -4132292_9308965_8742354 -4132292_9308965_8742354 kind=MOON orbit=0 radius=1.2472387850420885 starId=-1248503277 frame=false + body -4132292_9308965_8742354 -4132292_9308965_8742354 kind=ROGUE_PLANET orbit=0 radius=0.26084449443894825 starId=-1248503277 frame=true + body -4327181_-2482088_8170025 -4326270_-2482086_8172322 kind=MOON orbit=13213 radius=0.4108802270644634 starId=-368134761 frame=false + body -4327181_-2482088_8170025 -4326270_-2482086_8172322 kind=MOON orbit=13213 radius=0.7019849220580543 starId=-368134761 frame=false + body -4327181_-2482088_8170025 -4326270_-2482086_8172322 kind=PLANET orbit=13213 radius=1.9807050957565984 starId=-368134761 frame=true + body -4327181_-2482088_8170025 -4326631_-2482130_8171090 kind=PLANET orbit=6413 radius=1.7911161170913508 starId=-368134761 frame=true + body -4327181_-2482088_8170025 -4326779_-2482104_8170062 kind=MOON orbit=2161 radius=0.24291122518706265 starId=-368134761 frame=false + body -4327181_-2482088_8170025 -4326779_-2482104_8170062 kind=PLANET orbit=2161 radius=1.375903122981076 starId=-368134761 frame=true + body -4327181_-2482088_8170025 -4327072_-2482086_8169891 kind=PLANET orbit=924 radius=1.233461165549056 starId=-368134761 frame=true + body -4327181_-2482088_8170025 -4327118_-2482091_8170311 kind=PLANET orbit=1564 radius=0.31572437773244605 starId=-368134761 frame=true + body -4327181_-2482088_8170025 -4327146_-2482088_8170007 kind=STAR orbit=208 radius=143.9173023247719 starId=-368134763 frame=true + body -4327181_-2482088_8170025 -4327181_-2482088_8170025 kind=STAR orbit=0 radius=0.0 starId=-368134761 frame=true + body -4327181_-2482088_8170025 -4327185_-2482088_8170026 kind=STAR orbit=21 radius=133.5994808936119 starId=-368134762 frame=true + body -4327181_-2482088_8170025 -4327775_-2482075_8169571 kind=MOON orbit=3997 radius=0.4317252201804453 starId=-368134761 frame=false + body -4327181_-2482088_8170025 -4327775_-2482075_8169571 kind=PLANET orbit=3997 radius=0.7437095171035699 starId=-368134761 frame=true + body -4327181_-2482088_8170025 -4328444_-2482075_8173771 kind=ASTEROID_BELT orbit=21140 radius=0.0 starId=-368134761 frame=true + body -4460756_6796900_-655127 -4460756_6796900_-655127 kind=ROGUE_PLANET orbit=0 radius=0.9103973231499702 starId=-195625769 frame=true + body -4890828_1083961_9150975 -4890828_1083961_9150975 kind=MOON orbit=0 radius=0.37614864330229103 starId=-972281605 frame=false + body -4890828_1083961_9150975 -4890828_1083961_9150975 kind=ROGUE_PLANET orbit=0 radius=0.4227515483926527 starId=-972281605 frame=true + body -710985_6200792_981113 -710985_6200792_981113 kind=ROGUE_PLANET orbit=0 radius=0.927347427089166 starId=-1780361145 frame=true + body -728895_4809966_2398711 -728895_4809966_2398711 kind=MOON orbit=0 radius=0.4123351641483745 starId=-1402814529 frame=false + body -728895_4809966_2398711 -728895_4809966_2398711 kind=ROGUE_PLANET orbit=0 radius=1.5241224321858193 starId=-1402814529 frame=true + body 1112499_9349070_8700564 1112351_9349078_8700727 kind=PLANET orbit=1178 radius=1.3538830489987466 starId=-1857127685 frame=true + body 1112499_9349070_8700564 1112480_9349069_8700595 kind=MOON orbit=194 radius=0.34138769245275835 starId=-1857127685 frame=false + body 1112499_9349070_8700564 1112480_9349069_8700595 kind=MOON orbit=194 radius=0.6800089954407418 starId=-1857127685 frame=false + body 1112499_9349070_8700564 1112480_9349069_8700595 kind=PLANET orbit=194 radius=0.3505956426518401 starId=-1857127685 frame=true + body 1112499_9349070_8700564 1112480_9349070_8700517 kind=ASTEROID_BELT orbit=270 radius=0.0 starId=-1857127685 frame=true + body 1112499_9349070_8700564 1112488_9349070_8700570 kind=MOON orbit=68 radius=0.41066276981653405 starId=-1857127685 frame=false + body 1112499_9349070_8700564 1112488_9349070_8700570 kind=PLANET orbit=68 radius=1.1542418946073414 starId=-1857127685 frame=true + body 1112499_9349070_8700564 1112498_9349070_8700557 kind=PLANET orbit=38 radius=1.206794306592984 starId=-1857127685 frame=true + body 1112499_9349070_8700564 1112499_9349070_8700564 kind=STAR orbit=0 radius=0.0 starId=-1857127685 frame=true + body 1112499_9349070_8700564 1112516_9349054_8700047 kind=MOON orbit=2767 radius=0.2053795229785073 starId=-1857127685 frame=false + body 1112499_9349070_8700564 1112516_9349054_8700047 kind=MOON orbit=2767 radius=0.6597061290245418 starId=-1857127685 frame=false + body 1112499_9349070_8700564 1112516_9349054_8700047 kind=PLANET orbit=2767 radius=1.493144341488957 starId=-1857127685 frame=true + body 1112499_9349070_8700564 1112587_9349068_8700587 kind=GAS_GIANT orbit=486 radius=10.180912629340613 starId=-1857127685 frame=true + body 1112499_9349070_8700564 1112587_9349068_8700587 kind=MOON orbit=486 radius=0.23393678785827787 starId=-1857127685 frame=false + body 1112499_9349070_8700564 1112700_9349098_8701367 kind=ASTEROID_BELT orbit=4427 radius=0.0 starId=-1857127685 frame=true + body 1254743_817230_9170965 1254743_817230_9170965 kind=MOON orbit=0 radius=1.4906347986270239 starId=-1270778941 frame=false + body 1254743_817230_9170965 1254743_817230_9170965 kind=ROGUE_PLANET orbit=0 radius=0.21061591475977812 starId=-1270778941 frame=true + body 1388875_8027832_-500528 1388875_8027832_-500528 kind=ROGUE_PLANET orbit=0 radius=0.23853312255247255 starId=-911033505 frame=true + body 1925876_-960979_-1241912 1925279_-960989_-1242304 kind=ASTEROID_BELT orbit=3820 radius=0.0 starId=-1490136521 frame=true + body 1925876_-960979_-1241912 1925834_-960976_-1241971 kind=ASTEROID_BELT orbit=389 radius=0.0 starId=-1490136521 frame=true + body 1925876_-960979_-1241912 1925867_-960979_-1241904 kind=PLANET orbit=68 radius=0.6836214962700502 starId=-1490136521 frame=true + body 1925876_-960979_-1241912 1925870_-960981_-1241861 kind=PLANET orbit=275 radius=0.627027231276017 starId=-1490136521 frame=true + body 1925876_-960979_-1241912 1925876_-960979_-1241912 kind=STAR orbit=0 radius=0.0 starId=-1490136521 frame=true + body 1925876_-960979_-1241912 1926001_-960976_-1241951 kind=GAS_GIANT orbit=701 radius=8.135927550986239 starId=-1490136521 frame=true + body 1925876_-960979_-1241912 1926131_-960995_-1242279 kind=PLANET orbit=2388 radius=0.41444654488793814 starId=-1490136521 frame=true + body 2214240_3293070_-1049364 2214168_3293065_-1049284 kind=ASTEROID_BELT orbit=579 radius=0.0 starId=-1235117197 frame=true + body 2214240_3293070_-1049364 2214176_3293073_-1049342 kind=GAS_GIANT orbit=362 radius=5.400656412552245 starId=-1235117197 frame=true + body 2214240_3293070_-1049364 2214217_3293069_-1049393 kind=ASTEROID_BELT orbit=201 radius=0.0 starId=-1235117197 frame=true + body 2214240_3293070_-1049364 2214235_3293070_-1049372 kind=PLANET orbit=50 radius=0.9514717581328163 starId=-1235117197 frame=true + body 2214240_3293070_-1049364 2214238_3293070_-1049363 kind=PLANET orbit=10 radius=0.7217870748080282 starId=-1235117197 frame=true + body 2214240_3293070_-1049364 2214240_3293070_-1049364 kind=STAR orbit=0 radius=0.0 starId=-1235117197 frame=true + body 2214240_3293070_-1049364 2214244_3293070_-1049361 kind=PLANET orbit=28 radius=0.43863590730784535 starId=-1235117197 frame=true + body 2214240_3293070_-1049364 2214257_3293069_-1049347 kind=MOON orbit=128 radius=0.20152057506693566 starId=-1235117197 frame=false + body 2214240_3293070_-1049364 2214257_3293069_-1049347 kind=MOON orbit=128 radius=0.5060635739817203 starId=-1235117197 frame=false + body 2214240_3293070_-1049364 2214257_3293069_-1049347 kind=PLANET orbit=128 radius=0.8978675689008826 starId=-1235117197 frame=true + body 2857322_-315320_8342875 2857322_-315320_8342875 kind=ROGUE_PLANET orbit=0 radius=0.4250824398370271 starId=-369050573 frame=true + body 4028836_6182067_3322511 4028836_6182067_3322511 kind=MOON orbit=0 radius=0.20713736867197932 starId=-679746033 frame=false + body 4028836_6182067_3322511 4028836_6182067_3322511 kind=MOON orbit=0 radius=2.4431803509202763 starId=-679746033 frame=false + body 4028836_6182067_3322511 4028836_6182067_3322511 kind=ROGUE_PLANET orbit=0 radius=2.3281788126158585 starId=-679746033 frame=true + body 4390556_605832_3499725 4390486_605831_3499932 kind=PLANET orbit=1169 radius=1.9237313197528343 starId=-1579160837 frame=true + body 4390556_605832_3499725 4390541_605830_3499781 kind=GAS_GIANT orbit=310 radius=6.657802907195206 starId=-1579160837 frame=true + body 4390556_605832_3499725 4390541_605830_3499781 kind=MOON orbit=310 radius=0.2844906599851585 starId=-1579160837 frame=false + body 4390556_605832_3499725 4390541_605830_3499781 kind=MOON orbit=310 radius=0.3507788328860036 starId=-1579160837 frame=false + body 4390556_605832_3499725 4390541_605830_3499781 kind=MOON orbit=310 radius=0.5348901167995258 starId=-1579160837 frame=false + body 4390556_605832_3499725 4390556_605832_3499712 kind=MOON orbit=71 radius=0.21488918641162602 starId=-1579160837 frame=false + body 4390556_605832_3499725 4390556_605832_3499712 kind=MOON orbit=71 radius=0.3079528670494941 starId=-1579160837 frame=false + body 4390556_605832_3499725 4390556_605832_3499712 kind=PLANET orbit=71 radius=1.4602513571395628 starId=-1579160837 frame=true + body 4390556_605832_3499725 4390556_605832_3499725 kind=STAR orbit=0 radius=0.0 starId=-1579160837 frame=true + body 4390556_605832_3499725 4390573_605847_3500882 kind=PLANET orbit=6186 radius=1.511392524350423 starId=-1579160837 frame=true + body 4390556_605832_3499725 4390588_605833_3499721 kind=ASTEROID_BELT orbit=172 radius=0.0 starId=-1579160837 frame=true + body 4390556_605832_3499725 4392124_605915_3500704 kind=ASTEROID_BELT orbit=9897 radius=0.0 starId=-1579160837 frame=true + body 5275734_2263955_4887846 5275734_2263955_4887846 kind=ROGUE_PLANET orbit=0 radius=0.20440075971027813 starId=-1932462241 frame=true + body 6002622_-1792397_1902427 6002622_-1792397_1902427 kind=MOON orbit=0 radius=0.204261590532661 starId=-744566785 frame=false + body 6002622_-1792397_1902427 6002622_-1792397_1902427 kind=ROGUE_PLANET orbit=0 radius=0.9096911216480896 starId=-744566785 frame=true + body 6004696_9339127_232393 6004672_9339127_232357 kind=PLANET orbit=231 radius=2.357264927873378 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004686_9339127_232392 kind=MOON orbit=53 radius=0.4701363463386743 starId=-1711371857 frame=false + body 6004696_9339127_232393 6004686_9339127_232392 kind=PLANET orbit=53 radius=0.642853365230426 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004693_9339127_232391 kind=PLANET orbit=21 radius=1.089837967281112 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004696_9339123_232478 kind=GAS_GIANT orbit=457 radius=4.125788820472226 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004696_9339127_232393 kind=STAR orbit=0 radius=0.0 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004696_9339127_232394 kind=MOON orbit=7 radius=0.2921120563045062 starId=-1711371857 frame=false + body 6004696_9339127_232393 6004696_9339127_232394 kind=MOON orbit=7 radius=0.5129551242333938 starId=-1711371857 frame=false + body 6004696_9339127_232393 6004696_9339127_232394 kind=PLANET orbit=7 radius=0.9470450377905923 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004696_9339127_232395 kind=MOON orbit=11 radius=0.29330678016409184 starId=-1711371857 frame=false + body 6004696_9339127_232393 6004696_9339127_232395 kind=MOON orbit=11 radius=0.514929324927059 starId=-1711371857 frame=false + body 6004696_9339127_232393 6004696_9339127_232395 kind=PLANET orbit=11 radius=0.9405115620044691 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004698_9339127_232374 kind=MOON orbit=101 radius=0.6776233155918227 starId=-1711371857 frame=false + body 6004696_9339127_232393 6004698_9339127_232374 kind=PLANET orbit=101 radius=0.21443564201255633 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004701_9339127_232388 kind=PLANET orbit=39 radius=0.5701729389426105 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004711_9339127_232402 kind=ASTEROID_BELT orbit=96 radius=0.0 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004723_9339127_232710 kind=STAR orbit=1703 radius=106.38669212222099 starId=-1711371858 frame=true + body 6004696_9339127_232393 6004728_9339126_232398 kind=GAS_GIANT orbit=174 radius=6.557324680850876 starId=-1711371857 frame=true + body 6004696_9339127_232393 6004738_9339122_232263 kind=ASTEROID_BELT orbit=731 radius=0.0 starId=-1711371857 frame=true + body 6582583_-4715080_-2906356 6582583_-4715080_-2906356 kind=ROGUE_PLANET orbit=0 radius=0.35124043428512763 starId=-326789457 frame=true + body 7907701_7890188_-1932723 7907701_7890188_-1932723 kind=ROGUE_PLANET orbit=0 radius=0.3558343786814908 starId=-1524968341 frame=true + body 8875024_4661563_-4247235 8874321_4661577_-4247372 kind=MOON orbit=3831 radius=0.4501159637054517 starId=-1543301561 frame=false + body 8875024_4661563_-4247235 8874321_4661577_-4247372 kind=PLANET orbit=3831 radius=2.2945996126426924 starId=-1543301561 frame=true + body 8875024_4661563_-4247235 8874986_4661563_-4247263 kind=STAR orbit=253 radius=120.14879344582558 starId=-1543301562 frame=true + body 8875024_4661563_-4247235 8875017_4661563_-4247238 kind=PLANET orbit=40 radius=0.7255437882642279 starId=-1543301561 frame=true + body 8875024_4661563_-4247235 8875024_4661563_-4247235 kind=STAR orbit=0 radius=0.0 starId=-1543301561 frame=true + body 8875024_4661563_-4247235 8875042_4661566_-4246951 kind=PLANET orbit=1521 radius=1.8172110521780322 starId=-1543301561 frame=true + body 8875024_4661563_-4247235 8875090_4661521_-4246092 kind=ASTEROID_BELT orbit=6129 radius=0.0 starId=-1543301561 frame=true + body 8917875_8694620_8962618 8917875_8694620_8962618 kind=ROGUE_PLANET orbit=0 radius=1.0421460729938614 starId=-915858833 frame=true + body 902266_-3556779_3602641 902263_-3556779_3602643 kind=MOON orbit=17 radius=0.2114290419344704 starId=-231618813 frame=false + body 902266_-3556779_3602641 902263_-3556779_3602643 kind=MOON orbit=17 radius=0.42991321352492873 starId=-231618813 frame=false + body 902266_-3556779_3602641 902263_-3556779_3602643 kind=PLANET orbit=17 radius=2.135197106736538 starId=-231618813 frame=true + body 902266_-3556779_3602641 902264_-3556779_3602630 kind=MOON orbit=62 radius=0.249259439662258 starId=-231618813 frame=false + body 902266_-3556779_3602641 902264_-3556779_3602630 kind=MOON orbit=62 radius=0.25170238149933355 starId=-231618813 frame=false + body 902266_-3556779_3602641 902264_-3556779_3602630 kind=PLANET orbit=62 radius=0.2367929860706314 starId=-231618813 frame=true + body 902266_-3556779_3602641 902265_-3556779_3602639 kind=PLANET orbit=11 radius=2.1133071269095636 starId=-231618813 frame=true + body 902266_-3556779_3602641 902266_-3556779_3602641 kind=STAR orbit=0 radius=0.0 starId=-231618813 frame=true + body 902266_-3556779_3602641 902275_-3556777_3602589 kind=PLANET orbit=280 radius=1.96376357855455 starId=-231618813 frame=true + body 902266_-3556779_3602641 902284_-3556780_3602637 kind=MOON orbit=99 radius=0.6885499955857806 starId=-231618813 frame=false + body 902266_-3556779_3602641 902284_-3556780_3602637 kind=PLANET orbit=99 radius=0.44015553033561494 starId=-231618813 frame=true + body 902266_-3556779_3602641 902312_-3556778_3602711 kind=ASTEROID_BELT orbit=448 radius=0.0 starId=-231618813 frame=true + body 9414939_-4025819_8541847 9413578_-4025859_8545318 kind=GAS_GIANT orbit=19937 radius=6.972276140400643 starId=-1030351829 frame=true + body 9414939_-4025819_8541847 9414674_-4025830_8541837 kind=ASTEROID_BELT orbit=1421 radius=0.0 starId=-1030351829 frame=true + body 9414939_-4025819_8541847 9414855_-4025835_8541376 kind=GAS_GIANT orbit=2559 radius=5.97251947898175 starId=-1030351829 frame=true + body 9414939_-4025819_8541847 9414934_-4025819_8541970 kind=STAR orbit=660 radius=112.918562053442 starId=-1030351830 frame=true + body 9414939_-4025819_8541847 9414939_-4025819_8541847 kind=STAR orbit=0 radius=0.0 starId=-1030351829 frame=true + body 9414939_-4025819_8541847 9420876_-4025560_8542366 kind=ASTEROID_BELT orbit=31899 radius=0.0 starId=-1030351829 frame=true + body 9684198_4380843_7885519 9684198_4380843_7885519 kind=MOON orbit=0 radius=0.217218126585063 starId=-148558149 frame=false + body 9684198_4380843_7885519 9684198_4380843_7885519 kind=MOON orbit=0 radius=1.154401378900373 starId=-148558149 frame=false + body 9684198_4380843_7885519 9684198_4380843_7885519 kind=ROGUE_PLANET orbit=0 radius=0.6238414723722325 starId=-148558149 frame=true + derived -1113821_-4579746_-1376000 -1113821_-4579746_-1376000 type=ice mass=2.6080738648000046 radius=1.2451548691334666 gravity=168 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=40037 metallicity=0.934755314038844 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135434_2621617_-4213557 type=barren mass=0.003368670586447824 radius=0.2053908857293373 gravity=8 pressure=0 tempK=99 oxygen=false locked=false rings=false rotation=56659 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135440_2621614_-4213505 type=ice mass=1.481066875554912 radius=1.0776831807317033 gravity=128 pressure=1600 tempK=164 oxygen=false locked=false rings=false rotation=87805 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135484_2621613_-4213564 type=greenhouse mass=18.452700288558923 radius=2.3681492037108587 gravity=329 pressure=1600 tempK=290 oxygen=false locked=false rings=false rotation=24347 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135493_2621613_-4213737 type=icegiant mass=168.51954248437815 radius=8.346217680560606 gravity=242 pressure=1600 tempK=129 oxygen=false locked=false rings=true rotation=7468 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135498_2621613_-4213585 type=ice mass=0.002549949635953622 radius=0.2004764575690401 gravity=6 pressure=0 tempK=170 oxygen=false locked=false rings=false rotation=65034 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135505_2621614_-4213571 type=desert mass=1.8555431410975376 radius=1.124285627698082 gravity=147 pressure=92 tempK=572 oxygen=false locked=true rings=false rotation=31157 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135507_2621614_-4213570 type=lava mass=1.074139711744183 radius=1.050775516633949 gravity=97 pressure=1 tempK=1996 oxygen=false locked=true rings=true rotation=84517 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135508_2621614_-4213565 type=barren mass=0.0025500744378959034 radius=0.20468922867125786 gravity=6 pressure=0 tempK=382 oxygen=false locked=true rings=false rotation=27163 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135586_2621624_-4213820 type=ice mass=11.779809090176194 radius=2.023111892962628 gravity=288 pressure=1600 tempK=97 oxygen=false locked=false rings=false rotation=19040 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1135838_2621597_-4213313 type=ice mass=1.2786757557684811 radius=1.110681274660967 gravity=104 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=75020 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1135507_2621614_-4213570 -1137206_2621614_-4193101 type=gasgiant mass=84.75185176231003 radius=6.1902277988256245 gravity=221 pressure=1600 tempK=12 oxygen=false locked=false rings=true rotation=5794 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3348450_-3083316_687546 -3348450_-3083316_687546 type=ice mass=0.7895980120698235 radius=0.9166965049835585 gravity=94 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=7530 metallicity=1.4522943313876246 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4132292_9308965_8742354 -4132292_9308965_8742354 type=ice mass=0.006787266085944213 radius=0.26084449443894825 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=36025 metallicity=1.3962911538202278 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4326270_-2482086_8172322 type=superearth mass=11.741516846238405 radius=1.9807050957565984 gravity=299 pressure=1600 tempK=105 oxygen=false locked=false rings=false rotation=79673 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4326631_-2482130_8171090 type=ice mass=9.991527950948676 radius=1.7911161170913508 gravity=311 pressure=1600 tempK=131 oxygen=false locked=false rings=false rotation=68697 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4326779_-2482104_8170062 type=exotic mass=2.8472500726464136 radius=1.375903122981076 gravity=150 pressure=1600 tempK=260 oxygen=false locked=false rings=false rotation=10880 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4327072_-2482086_8169891 type=greenhouse mass=2.2878431586379064 radius=1.233461165549056 gravity=150 pressure=1600 tempK=306 oxygen=false locked=false rings=false rotation=50509 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4327118_-2482091_8170311 type=ice mass=0.013195151898327581 radius=0.31572437773244605 gravity=13 pressure=0 tempK=118 oxygen=false locked=false rings=false rotation=7367 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4327146_-2482088_8170007 type=desert mass=0.021984798863282966 radius=0.3494094803570291 gravity=18 pressure=0 tempK=350 oxygen=false locked=false rings=false rotation=49236 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4327181_-2482088_8170025 type=lava mass=23.501518662832556 radius=2.3580345746047584 gravity=400 pressure=67 tempK=4851 oxygen=false locked=true rings=false rotation=24155 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4327185_-2482088_8170026 type=lava mass=15.617620240213085 radius=2.0737052949692467 gravity=363 pressure=245 tempK=1495 oxygen=false locked=true rings=false rotation=10671 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4327775_-2482075_8169571 type=ice mass=0.2847827010675529 radius=0.7437095171035699 gravity=51 pressure=158 tempK=93 oxygen=false locked=false rings=false rotation=37617 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4327181_-2482088_8170025 -4328444_-2482075_8173771 type=ice mass=24.904024376060658 radius=2.298694700106472 gravity=400 pressure=1600 tempK=72 oxygen=false locked=false rings=false rotation=48629 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4460756_6796900_-655127 -4460756_6796900_-655127 type=ice mass=0.8515631132605336 radius=0.9103973231499702 gravity=103 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=22874 metallicity=0.6054647811761072 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -4890828_1083961_9150975 -4890828_1083961_9150975 type=barren mass=0.036172393673821295 radius=0.4227515483926527 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=15728 metallicity=0.8447497217111585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -710985_6200792_981113 -710985_6200792_981113 type=ice mass=0.8717414829472112 radius=0.927347427089166 gravity=101 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=11959 metallicity=0.35374578149423735 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -728895_4809966_2398711 -728895_4809966_2398711 type=superearth mass=5.842756906230048 radius=1.5241224321858193 gravity=252 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=42107 metallicity=0.4096887292822291 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1112499_9349070_8700564 1112351_9349078_8700727 type=ice mass=3.1702058952069962 radius=1.3538830489987466 gravity=173 pressure=1600 tempK=146 oxygen=false locked=false rings=false rotation=6364 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1112499_9349070_8700564 1112480_9349069_8700595 type=barren mass=0.01609332936099729 radius=0.3505956426518401 gravity=13 pressure=0 tempK=195 oxygen=false locked=false rings=false rotation=39980 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1112499_9349070_8700564 1112480_9349070_8700517 type=gasgiant mass=263.9517328332345 radius=10.144196205432351 gravity=257 pressure=1600 tempK=323 oxygen=false locked=false rings=false rotation=11912 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1112499_9349070_8700564 1112488_9349070_8700570 type=greenhouse mass=2.047291102547288 radius=1.1542418946073414 gravity=154 pressure=689 tempK=439 oxygen=false locked=false rings=false rotation=26746 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1112499_9349070_8700564 1112498_9349070_8700557 type=greenhouse mass=2.388601888602027 radius=1.206794306592984 gravity=164 pressure=243 tempK=452 oxygen=false locked=true rings=false rotation=68997 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1112499_9349070_8700564 1112499_9349070_8700564 type=lava mass=0.14621969817118385 radius=0.6086364441260886 gravity=39 pressure=0 tempK=2736 oxygen=false locked=true rings=false rotation=14411 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1112499_9349070_8700564 1112516_9349054_8700047 type=ice mass=3.800366081479916 radius=1.493144341488957 gravity=170 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=24054 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1112499_9349070_8700564 1112587_9349068_8700587 type=icegiant mass=266.1542332383445 radius=10.180912629340613 gravity=257 pressure=1600 tempK=241 oxygen=false locked=false rings=false rotation=7491 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1112499_9349070_8700564 1112700_9349098_8701367 type=ice mass=0.0123337512601163 radius=0.29596529314031295 gravity=14 pressure=3 tempK=33 oxygen=false locked=false rings=false rotation=7042 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1254743_817230_9170965 1254743_817230_9170965 type=barren mass=0.0037430375605988766 radius=0.21061591475977812 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=7253 metallicity=1.1148507197072983 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1388875_8027832_-500528 1388875_8027832_-500528 type=ice mass=0.006116930500553493 radius=0.23853312255247255 gravity=11 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=16811 metallicity=0.6973509409375414 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1925876_-960979_-1241912 1925279_-960989_-1242304 type=ice mass=0.0030659681983630305 radius=0.21142585317222323 gravity=7 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=43679 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1925876_-960979_-1241912 1925834_-960976_-1241971 type=barren mass=0.10065146235216979 radius=0.5795739018707657 gravity=30 pressure=5 tempK=152 oxygen=false locked=false rings=false rotation=93186 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1925876_-960979_-1241912 1925867_-960979_-1241904 type=desert mass=0.28795346964131435 radius=0.6836214962700502 gravity=62 pressure=2 tempK=344 oxygen=false locked=false rings=false rotation=68436 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1925876_-960979_-1241912 1925870_-960981_-1241861 type=ice mass=0.19230972517521197 radius=0.627027231276017 gravity=49 pressure=10 tempK=148 oxygen=false locked=false rings=false rotation=31018 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1925876_-960979_-1241912 1925876_-960979_-1241912 type=lava mass=0.003807051127491194 radius=0.23331342623334905 gravity=7 pressure=0 tempK=3023 oxygen=false locked=true rings=false rotation=22920 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1925876_-960979_-1241912 1926001_-960976_-1241951 type=icegiant mass=158.91328712368997 radius=8.135927550986239 gravity=240 pressure=1600 tempK=221 oxygen=false locked=false rings=true rotation=6488 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1925876_-960979_-1241912 1926131_-960995_-1242279 type=barren mass=0.029325698836969893 radius=0.41444654488793814 gravity=17 pressure=5 tempK=61 oxygen=false locked=false rings=false rotation=42102 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2214240_3293070_-1049364 2214168_3293065_-1049284 type=gasgiant mass=167.6182979811282 radius=8.326781453728056 gravity=242 pressure=1600 tempK=80 oxygen=false locked=false rings=true rotation=6485 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2214240_3293070_-1049364 2214176_3293073_-1049342 type=icegiant mass=61.92285986642407 radius=5.400656412552245 gravity=212 pressure=1600 tempK=101 oxygen=false locked=false rings=false rotation=9010 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2214240_3293070_-1049364 2214217_3293069_-1049393 type=icegiant mass=93.7280033148562 radius=6.467186727479397 gravity=224 pressure=1600 tempK=136 oxygen=false locked=false rings=true rotation=5260 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2214240_3293070_-1049364 2214235_3293070_-1049372 type=exotic mass=0.9039005442391228 radius=0.9514717581328163 gravity=100 pressure=602 tempK=233 oxygen=false locked=false rings=false rotation=20452 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2214240_3293070_-1049364 2214238_3293070_-1049363 type=barren mass=0.31856247810292343 radius=0.7217870748080282 gravity=61 pressure=18 tempK=313 oxygen=false locked=true rings=false rotation=51889 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2214240_3293070_-1049364 2214240_3293070_-1049364 type=barren mass=0.11684115568334545 radius=0.5612053102156213 gravity=37 pressure=0 tempK=990 oxygen=false locked=true rings=false rotation=75825 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2214240_3293070_-1049364 2214244_3293070_-1049361 type=ice mass=0.056512753428701996 radius=0.43863590730784535 gravity=29 pressure=2 tempK=153 oxygen=false locked=true rings=false rotation=19706 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2214240_3293070_-1049364 2214257_3293069_-1049347 type=ice mass=0.8035144573944528 radius=0.8978675689008826 gravity=100 pressure=801 tempK=135 oxygen=false locked=false rings=false rotation=94688 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2857322_-315320_8342875 2857322_-315320_8342875 type=barren mass=0.04675124401442071 radius=0.4250824398370271 gravity=26 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=21193 metallicity=0.8215279711125094 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4028836_6182067_3322511 4028836_6182067_3322511 type=ice mass=25.185595027865407 radius=2.3281788126158585 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=6334 metallicity=0.9445001714977364 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4390556_605832_3499725 4390486_605831_3499932 type=superearth mass=13.169100524487668 radius=1.9237313197528343 gravity=356 pressure=1600 tempK=269 oxygen=false locked=false rings=false rotation=16737 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4390556_605832_3499725 4390541_605830_3499781 type=gasgiant mass=100.2040081010103 radius=6.657802907195206 gravity=226 pressure=1600 tempK=481 oxygen=false locked=false rings=false rotation=10072 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4390556_605832_3499725 4390556_605832_3499712 type=superearth mass=3.871910372129332 radius=1.4602513571395628 gravity=182 pressure=254 tempK=690 oxygen=false locked=false rings=false rotation=7164 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4390556_605832_3499725 4390556_605832_3499725 type=lava mass=4.134479878177709 radius=1.4847019055371198 gravity=188 pressure=1 tempK=4361 oxygen=false locked=true rings=false rotation=7884 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4390556_605832_3499725 4390573_605847_3500882 type=superearth mass=4.527987623408388 radius=1.511392524350423 gravity=198 pressure=1600 tempK=117 oxygen=false locked=false rings=false rotation=51841 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4390556_605832_3499725 4390588_605833_3499721 type=greenhouse mass=4.965325875969389 radius=1.5841652900266867 gravity=198 pressure=918 tempK=472 oxygen=false locked=false rings=false rotation=7049 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4390556_605832_3499725 4392124_605915_3500704 type=gasgiant mass=136.1770836540945 radius=7.607679548019192 gravity=235 pressure=1600 tempK=85 oxygen=false locked=false rings=true rotation=12251 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5275734_2263955_4887846 5275734_2263955_4887846 type=barren mass=0.0029962764594867377 radius=0.20440075971027813 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=8339 metallicity=1.0030073057735065 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6002622_-1792397_1902427 6002622_-1792397_1902427 type=ice mass=0.6573080231667359 radius=0.9096911216480896 gravity=79 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=23266 metallicity=1.5333359065190146 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004672_9339127_232357 type=ice mass=19.383326879044013 radius=2.357264927873378 gravity=349 pressure=1600 tempK=190 oxygen=false locked=false rings=false rotation=32277 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004686_9339127_232392 type=ice mass=0.18729186361623149 radius=0.642853365230426 gravity=45 pressure=46 tempK=126 oxygen=false locked=false rings=false rotation=16661 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004693_9339127_232391 type=exotic mass=1.6739824492680222 radius=1.089837967281112 gravity=141 pressure=503 tempK=374 oxygen=false locked=true rings=false rotation=12167 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004696_9339123_232478 type=gasgiant mass=33.334199633159535 radius=4.125788820472226 gravity=196 pressure=1600 tempK=190 oxygen=false locked=false rings=true rotation=9524 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004696_9339127_232393 type=lava mass=0.6522438446102877 radius=0.9536021273643882 gravity=72 pressure=2 tempK=1075 oxygen=false locked=true rings=false rotation=29084 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004696_9339127_232394 type=desert mass=0.6368905172168323 radius=0.9470450377905923 gravity=71 pressure=22 tempK=382 oxygen=false locked=true rings=false rotation=28448 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004696_9339127_232395 type=desert mass=0.6218442490287343 radius=0.9405115620044691 gravity=70 pressure=37 tempK=305 oxygen=false locked=true rings=false rotation=28658 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004698_9339127_232374 type=ice mass=0.0033572072851826727 radius=0.21443564201255633 gravity=7 pressure=0 tempK=99 oxygen=false locked=false rings=false rotation=22527 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004701_9339127_232388 type=barren mass=0.12453914538684117 radius=0.5701729389426105 gravity=38 pressure=14 tempK=175 oxygen=false locked=true rings=false rotation=25614 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004711_9339127_232402 type=barren mass=0.01054533698309216 radius=0.2765574870401879 gravity=14 pressure=0 tempK=123 oxygen=false locked=false rings=false rotation=6801 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004723_9339127_232710 type=gasgiant mass=298.52266955600913 radius=10.701828526351594 gravity=261 pressure=1600 tempK=160 oxygen=false locked=false rings=true rotation=6731 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004728_9339126_232398 type=icegiant mass=96.75988118720521 radius=6.557324680850876 gravity=225 pressure=1600 tempK=209 oxygen=false locked=false rings=true rotation=9441 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6004696_9339127_232393 6004738_9339122_232263 type=icegiant mass=209.33589845914716 radius=9.171563489691659 gravity=249 pressure=1600 tempK=183 oxygen=false locked=false rings=true rotation=5859 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6582583_-4715080_-2906356 6582583_-4715080_-2906356 type=ice mass=0.02221945063321854 radius=0.35124043428512763 gravity=18 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=25977 metallicity=1.047030230273708 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 7907701_7890188_-1932723 7907701_7890188_-1932723 type=ice mass=0.018383606993069193 radius=0.3558343786814908 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=29027 metallicity=0.8482433564262986 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8875024_4661563_-4247235 8874321_4661577_-4247372 type=superearth mass=18.99169895417755 radius=2.2945996126426924 gravity=361 pressure=1600 tempK=107 oxygen=false locked=false rings=false rotation=22548 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8875024_4661563_-4247235 8874986_4661563_-4247263 type=ice mass=0.053901646586943006 radius=0.46577495126394647 gravity=25 pressure=2 tempK=158 oxygen=false locked=false rings=true rotation=25891 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8875024_4661563_-4247235 8875017_4661563_-4247238 type=desert mass=0.25152217164469054 radius=0.7255437882642279 gravity=48 pressure=1 tempK=448 oxygen=false locked=true rings=false rotation=9830 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8875024_4661563_-4247235 8875024_4661563_-4247235 type=lava mass=0.002741328043620735 radius=0.2043323623311955 gravity=7 pressure=0 tempK=3018 oxygen=false locked=true rings=false rotation=77652 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8875024_4661563_-4247235 8875042_4661566_-4246951 type=ice mass=7.317659402671944 radius=1.8172110521780322 gravity=222 pressure=1600 tempK=148 oxygen=false locked=false rings=false rotation=14096 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8875024_4661563_-4247235 8875090_4661521_-4246092 type=gasgiant mass=16.82630813853038 radius=3.0649226527179056 gravity=179 pressure=1600 tempK=78 oxygen=false locked=false rings=true rotation=5787 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 8917875_8694620_8962618 8917875_8694620_8962618 type=ice mass=1.2777567430869878 radius=1.0421460729938614 gravity=118 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=48062 metallicity=1.0897872502867014 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 902266_-3556779_3602641 902263_-3556779_3602643 type=superearth mass=15.617220891812584 radius=2.135197106736538 gravity=343 pressure=1600 tempK=440 oxygen=false locked=true rings=false rotation=59572 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 902266_-3556779_3602641 902264_-3556779_3602630 type=ice mass=0.003953003032939178 radius=0.2367929860706314 gravity=7 pressure=0 tempK=89 oxygen=false locked=false rings=false rotation=6058 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 902266_-3556779_3602641 902265_-3556779_3602639 type=greenhouse mass=14.916611343283593 radius=2.1133071269095636 gravity=334 pressure=1600 tempK=422 oxygen=false locked=true rings=false rotation=15591 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 902266_-3556779_3602641 902266_-3556779_3602641 type=barren mass=0.0020358423726517545 radius=0.20047997099920922 gravity=5 pressure=0 tempK=853 oxygen=false locked=true rings=false rotation=14154 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 902266_-3556779_3602641 902275_-3556777_3602589 type=ice mass=14.414213798293174 radius=1.96376357855455 gravity=374 pressure=1600 tempK=94 oxygen=false locked=false rings=false rotation=8345 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 902266_-3556779_3602641 902284_-3556780_3602637 type=barren mass=0.05050336487176186 radius=0.44015553033561494 gravity=26 pressure=5 tempK=85 oxygen=false locked=false rings=false rotation=13592 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 902266_-3556779_3602641 902312_-3556778_3602711 type=icegiant mass=45.7142664842576 radius=4.7330656319620905 gravity=204 pressure=1600 tempK=78 oxygen=false locked=false rings=false rotation=6513 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9414939_-4025819_8541847 9413578_-4025859_8545318 type=gasgiant mass=111.42572700047312 radius=6.972276140400643 gravity=229 pressure=1600 tempK=111 oxygen=false locked=false rings=false rotation=14284 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9414939_-4025819_8541847 9414674_-4025830_8541837 type=ice mass=0.2904384889155367 radius=0.7405269019493477 gravity=53 pressure=9 tempK=176 oxygen=false locked=false rings=false rotation=77597 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9414939_-4025819_8541847 9414855_-4025835_8541376 type=gasgiant mass=78.052418822225 radius=5.97251947898175 gravity=219 pressure=1600 tempK=312 oxygen=false locked=false rings=true rotation=13836 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9414939_-4025819_8541847 9414934_-4025819_8541970 type=superearth mass=21.19914300628885 radius=2.445134938265847 gravity=355 pressure=1600 tempK=668 oxygen=false locked=false rings=false rotation=42946 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9414939_-4025819_8541847 9414939_-4025819_8541847 type=unclassified mass=0.08766661784211546 radius=0.5507068140293626 gravity=29 pressure=0 tempK=7629 oxygen=false locked=true rings=false rotation=15814 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9414939_-4025819_8541847 9420876_-4025560_8542366 type=barren mass=0.008210776742651797 radius=0.2679348082914701 gravity=11 pressure=1 tempK=45 oxygen=false locked=false rings=false rotation=91180 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 9684198_4380843_7885519 9684198_4380843_7885519 type=barren mass=0.13794446152220974 radius=0.6238414723722325 gravity=35 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=11524 metallicity=0.8696367147827484 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1113821_-4579746_-1376000 id=-237253153 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless + system -1135507_2621614_-4213570 id=-1468730469 kind=STAR name=PGS--5002361.0.-5002361 starTemp=70 starSize=1.0959510803222656 + system -3348450_-3083316_687546 id=-1251094673 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless + system -4132292_9308965_8742354 id=-1248503277 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless + system -4327181_-2482088_8170025 id=-368134761 kind=STAR name=PGS--5002361.-5002361.5002361 starTemp=150 starSize=1.3601762056350708 + system -4460756_6796900_-655127 id=-195625769 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless + system -4890828_1083961_9150975 id=-972281605 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless + system -710985_6200792_981113 id=-1780361145 kind=ROGUE_PLANET name=PGR--5002361.5002361.0 starless + system -728895_4809966_2398711 id=-1402814529 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless + system 1112499_9349070_8700564 id=-1857127685 kind=STAR name=PGS-0.5002361.5002361 starTemp=100 starSize=1.009089469909668 + system 1254743_817230_9170965 id=-1270778941 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless + system 1388875_8027832_-500528 id=-911033505 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless + system 1925876_-960979_-1241912 id=-1490136521 kind=STAR name=PGS-0.-5002361.-5002361 starTemp=100 starSize=1.2317028045654297 + system 2214240_3293070_-1049364 id=-1235117197 kind=STAR name=PGS-0.0.-5002361 starTemp=40 starSize=0.834796667098999 + system 2857322_-315320_8342875 id=-369050573 kind=ROGUE_PLANET name=PGR-0.-5002361.5002361 starless + system 4028836_6182067_3322511 id=-679746033 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless + system 4390556_605832_3499725 id=-1579160837 kind=STAR name=PGS-0.0.0 starTemp=150 starSize=1.138994812965393 + system 5275734_2263955_4887846 id=-1932462241 kind=ROGUE_PLANET name=PGR-5002361.0.0 starless + system 6002622_-1792397_1902427 id=-744566785 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless + system 6004696_9339127_232393 id=-1711371857 kind=STAR name=PGS-5002361.5002361.0 starTemp=40 starSize=0.974504828453064 + system 6582583_-4715080_-2906356 id=-326789457 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless + system 7907701_7890188_-1932723 id=-1524968341 kind=ROGUE_PLANET name=PGR-5002361.5002361.-5002361 starless + system 8875024_4661563_-4247235 id=-1543301561 kind=STAR name=PGS-5002361.0.-5002361 starTemp=100 starSize=1.227363109588623 + system 8917875_8694620_8962618 id=-915858833 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless + system 902266_-3556779_3602641 id=-231618813 kind=STAR name=PGS-0.-5002361.0 starTemp=40 starSize=0.6209897398948669 + system 9414939_-4025819_8541847 id=-1030351829 kind=STAR name=PGS-5002361.-5002361.5002361 starTemp=220 starSize=1.8374673128128052 + system 9684198_4380843_7885519 id=-148558149 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless From dc65ad4c2bbac303aa7dc079c79f0622dcbd30eb Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 19 Aug 2026 15:12:22 +0300 Subject: [PATCH 39/42] feat: a body's radius is what its neighbours are measured against - moon orbits drawn in parent radii, not absolute blocks - authored moon orbits floored above the parent surface - descent shell follows body radius plus a Karman atmosphere - entry ring derived from the shell, keeping the hysteresis - planet-view size scales by radius, mass indicator by mass - star weights reweighted to observed abundance by number - star lattice edge derived from separation and occupancy - galaxy and cluster densities stated as knobs, not readings - golden corpus regenerated against the new placement --- .../render/entity/RenderPlanetUIEntity.java | 9 +- .../dimension/DimensionProperties.java | 27 +- .../advancedRocketry/space/DescentShell.java | 37 +- .../space/ShipEntryController.java | 40 +- .../universe/ClusteredGalaxyGenerator.java | 37 +- .../universe/GalaxyGenConfig.java | 73 +- .../universe/SystemContent.java | 15 +- .../universe/UniverseRegistry.java | 22 + .../universe/UniverseScale.java | 56 +- .../unit/ClusteredGalaxyGeneratorTest.java | 148 +- .../test/unit/DescentShellTest.java | 43 + .../test/unit/DriveLadderTest.java | 16 +- .../unit/InterstellarLegDistanceTest.java | 16 +- .../test/unit/TelescopeRegionScanTest.java | 38 +- .../resources/universe/golden-corpus-v1.txt | 3243 +++++++++-------- 15 files changed, 2274 insertions(+), 1546 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/client/render/entity/RenderPlanetUIEntity.java b/src/main/java/zmaster587/advancedRocketry/client/render/entity/RenderPlanetUIEntity.java index 65ec1cd57..5e00a82cc 100644 --- a/src/main/java/zmaster587/advancedRocketry/client/render/entity/RenderPlanetUIEntity.java +++ b/src/main/java/zmaster587/advancedRocketry/client/render/entity/RenderPlanetUIEntity.java @@ -55,7 +55,10 @@ public void doRender(EntityUIPlanet entity, double x, double y, double z, if (properties == null) return; - float sizeScale = Math.max(properties.gravitationalMultiplier * properties.gravitationalMultiplier * entity.getScale(), .5f); + // Scaled by the body's RADIUS, not by gravity squared: gravity is derived from mass and radius, + // so sizing by g² sizes by mass²/radius⁴ and draws a dense small world larger than a big light + // one. A radius is what a drawn size is. + float sizeScale = Math.max((float) Math.max(properties.getRadius(), 0.5d) * entity.getScale(), .5f); GL11.glPushMatrix(); GL11.glTranslatef((float) x, (float) y + sizeScale * 0.03f, (float) z); @@ -187,7 +190,9 @@ public void doRender(EntityUIPlanet entity, double x, double y, double z, //Draw Mass indicator Minecraft.getMinecraft().renderEngine.bindTexture(planetUIFG); GlStateManager.color(1, 1, 1, 0.8f); - renderMassIndicator(buffer, Math.min(properties.gravitationalMultiplier / 2f, 1f)); + // The MASS indicator reads the mass. It read gravity, which is a different quantity and + // has been separately stored since mass became a primary property. + renderMassIndicator(buffer, (float) Math.min(properties.getMass() / 2d, 1d)); //Draw background GlStateManager.color(1, 1, 1, 1); diff --git a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java index 75c8e7afa..9305ed61d 100644 --- a/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java +++ b/src/main/java/zmaster587/advancedRocketry/dimension/DimensionProperties.java @@ -2750,12 +2750,35 @@ public boolean canGenerateCaves() { return this.canGenerateCaves; } + /** + * How big this world is drawn in the planet view. + * + *

    It follows the body's RADIUS, which is what a drawn size is. It used to be + * {@code max(g², 0.5)} — a size synthesised from gravity, which is not a size — and that was a + * necessary approximation only while a planet had no radius of its own. It has had one since mass + * and radius became primary properties, and gravity is now DERIVED from them, so sizing by gravity + * squared means sizing by mass²/radius⁴: a dense small world drew larger than a big light one.

    + * + *

    The floor and the per-kind factors are unchanged, so an Earth-sized world (radius 1) draws + * exactly as it did — what moves is everything that is not Earth-sized.

    + */ public float getRenderSizePlanetView() { - return (isMoon() ? 8f : 10f) * Math.max(this.getGravitationalMultiplier() * this.getGravitationalMultiplier(), .5f) * 100; + return (isMoon() ? 8f : 10f) * renderRadiusFactor() * 100; } + /** The same, in the solar view, where a moon is drawn much smaller against its system. */ public float getRenderSizeSolarView() { - return (isMoon() ? 0.2f : 1f) * Math.max(this.getGravitationalMultiplier() * this.getGravitationalMultiplier(), .5f) * 100; + return (isMoon() ? 0.2f : 1f) * renderRadiusFactor() * 100; + } + + /** + * The body's radius in Earth radii, floored — the one quantity both views scale by. A world with + * no stated bulk falls back to one Earth radius, which is what an unstated bulk describes + * everywhere else in this layer. + */ + private float renderRadiusFactor() { + double r = getRadius(); + return (float) Math.max(r > 0d ? r : 1d, 0.5d); } // Relative to parent diff --git a/src/main/java/zmaster587/advancedRocketry/space/DescentShell.java b/src/main/java/zmaster587/advancedRocketry/space/DescentShell.java index be6f60e98..d0f79e57f 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/DescentShell.java +++ b/src/main/java/zmaster587/advancedRocketry/space/DescentShell.java @@ -1,5 +1,6 @@ package zmaster587.advancedRocketry.space; +import zmaster587.advancedRocketry.util.AstronomicalBodyHelper; import zmaster587.advancedRocketry.universe.SystemBody; /** @@ -33,10 +34,44 @@ private DescentShell() { * adds the atmosphere's own depth; nothing else has to change, which is the whole reason this * method exists rather than the constant being read at each call site.

    */ + /** + * How high above {@code body}'s centre its atmosphere ends — the surface a descent triggers at. + * + *

    It is the body's own radius plus an atmosphere, and that is a change of kind. This + * used to ignore its argument and return a flat {@code DESCENT_RADIUS_BLOCKS} = 512, chosen when a + * body had no size at all. Once bodies got a real radius that constant became 1/50 of an Earth + * (25 513 blocks) and 1/548 of a Jupiter (280 643): the boundary a descent fires at lay deep INSIDE + * the world it belongs to, so a pilot flew through the whole bulk before anything happened and + * {@link #distanceToShell} — the number an approach read-out is built on — described a sphere + * nowhere near where the world ends.

    + * + *

    The atmosphere fraction is measured, not chosen: the Kármán line stands at 100 km over + * an Earth radius of 6 371 km, i.e. 1.57 % above the surface, and that ratio is what + * {@link #ATMOSPHERE_FRACTION} states. A world twice the size gets a shell twice as far out, + * which is the property the flat constant could not have.

    + * + *

    A body with no radius keeps the flat radius, and that is not a fallback but the right + * answer: a belt or a station slot is not a sphere, has no surface to stand above, and the constant + * is then a proximity radius rather than an atmosphere.

    + */ public static long radiusAround(SystemBody body) { - return ShipEntryController.DESCENT_RADIUS_BLOCKS; + double radiusEarths = (body == null) ? 0d : body.radiusEarths(); + if (!(radiusEarths > 0d)) { + return ShipEntryController.DESCENT_RADIUS_BLOCKS; + } + double surfaceBlocks = radiusEarths * AstronomicalBodyHelper.EARTH_RADIUS_BLOCKS; + long shell = Math.round(surfaceBlocks * (1d + ATMOSPHERE_FRACTION)); + // Never below the flat radius: a body small enough that its atmosphere is thinner than the old + // proximity sphere still has to be approachable at the scale a ship manoeuvres in. + return Math.max(ShipEntryController.DESCENT_RADIUS_BLOCKS, shell); } + /** + * How far a world's atmosphere reaches above its surface, as a fraction of its radius — the Kármán + * line, 100 km over Earth's 6 371 km. + */ + public static final double ATMOSPHERE_FRACTION = 100d / 6371d; + /** * How far a ship at {@code distanceToCentre} blocks still has to travel before it crosses * {@code body}'s atmosphere — clamped at zero, because inside the shell there is nothing left diff --git a/src/main/java/zmaster587/advancedRocketry/space/ShipEntryController.java b/src/main/java/zmaster587/advancedRocketry/space/ShipEntryController.java index f587fb39a..0145b25fb 100644 --- a/src/main/java/zmaster587/advancedRocketry/space/ShipEntryController.java +++ b/src/main/java/zmaster587/advancedRocketry/space/ShipEntryController.java @@ -45,12 +45,44 @@ public final class ShipEntryController { public static final long DESCENT_RADIUS_BLOCKS = 512L; /** - * Entry spawn-ring distance from the launch body's POI (blocks, cell-local). MUST stay - * strictly greater than {@link #DESCENT_RADIUS_BLOCKS} — the entry↔descent hysteresis - * contract (an entering ship never spawns inside the descent trigger). {@code tunable}. + * Entry spawn-ring distance from the launch body's POI (blocks, cell-local), for a body with no + * size of its own. MUST stay strictly greater than {@link #DESCENT_RADIUS_BLOCKS} — the + * entry↔descent hysteresis contract (an entering ship never spawns inside the descent + * trigger). {@code tunable}. + * + *

    For a body that HAS a radius the ring follows the shell instead of this constant — see + * {@link #entryRingAround}. The hysteresis is a relation between the two, not a pair of numbers, + * and it stopped being expressible as a pair the moment the shell started depending on the body.

    */ public static final long ENTRY_RING_BLOCKS = DESCENT_RADIUS_BLOCKS * 2L; + /** + * The ring an entering ship spawns on around {@code body} — always strictly outside that body's + * descent shell, so a ship that has just entered is never already inside the trigger it is about + * to fly towards. + */ + public static long entryRingAround(zmaster587.advancedRocketry.universe.SystemBody body) { + long shell = zmaster587.advancedRocketry.space.DescentShell.radiusAround(body); + return Math.max(ENTRY_RING_BLOCKS, shell * 2L); + } + + /** + * The same ring for a body known only by ADDRESS — the entry path holds a coordinate, not the + * body object, so the body is resolved through the registry and the flat ring is used when there + * is nothing there to resolve (an unplaced launch, the config home anchor). + */ + public static long entryRingAround(GalacticCoord bodyAddress) { + if (bodyAddress == null) { + return ENTRY_RING_BLOCKS; + } + long widest = ENTRY_RING_BLOCKS; + for (zmaster587.advancedRocketry.universe.SystemBody b + : zmaster587.advancedRocketry.universe.UniverseRegistry.bodiesAtOnServer(bodyAddress)) { + widest = Math.max(widest, entryRingAround(b)); + } + return widest; + } + /** Ticks a ship waits after a refused/failed entry before the ceiling check may re-trigger. */ private static final int RETRY_COOLDOWN_TICKS = 100; @@ -297,7 +329,7 @@ private GalacticCoord resolveEntryCoord(int launchDimId, UUID shipId) { if (body == null) { body = GalacticCoord.ORIGIN; } - return StandoffRing.pointAround(body, ENTRY_RING_BLOCKS, shipId.hashCode()); + return StandoffRing.pointAround(body, entryRingAround(body), shipId.hashCode()); } /** Advance every in-flight entry one tick (the shared crossing settle loop). */ diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index f6ad0cdd1..d7f38f4e0 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -155,6 +155,19 @@ public final class ClusteredGalaxyGenerator implements IGalaxyGenerator { private static final int MAX_MOONS_GIANT = 5; private static final double MOON_COUNT_BIAS = 1.9d; /** A moon's orbit about its parent, in the parent-relative units the moon ephemeris is written in. */ + /** + * How far a moon orbits, in PARENT RADII — the band real satellite systems occupy, and the only + * form of this number that survives a body having a size. + * + *

    It used to be an absolute length ({@code MOON_MIN_ORBIT}..{@code +MOON_ORBIT_SPAN} units of + * 200 blocks, i.e. 4 000–26 000 blocks) chosen when a planet had no radius at all. Once bodies got + * one, an Earth stood 25 513 blocks across and a Jupiter 280 643 — so essentially every moon was + * seated INSIDE its parent, and a giant's by an order of magnitude. A multiple cannot express that + * failure: 2.5 radii is outside the surface whatever the body turns out to be.

    + */ + private static final double MOON_MIN_PARENT_RADII = 2.5d; + private static final double MOON_MAX_PARENT_RADII = 12d; + private static final int MOON_MIN_ORBIT = 20; private static final int MOON_ORBIT_SPAN = 110; @@ -451,8 +464,8 @@ private List rogueBodiesFor(long seed, GalacticCoord cell, int syste } CellFrame frame = CellFrame.staticAt(cell); for (int j = 1; j <= moons; j++) { - int moonOrbit = MOON_MIN_ORBIT + (int) (CellHash.norm( - CellHash.ofBody(seed, cell, j, SALT_ROGUE_MOONRAD)) * MOON_ORBIT_SPAN); + int moonOrbit = moonOrbitUnits(profile.radiusEarths(), + CellHash.norm(CellHash.ofBody(seed, cell, j, SALT_ROGUE_MOONRAD))); double theta = CellHash.norm(CellHash.ofBody(seed, cell, j, SALT_ROGUE_MOONANG)) * 2d * Math.PI; double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY @@ -687,6 +700,22 @@ private static void addBelt(List bodies, long seed, GalacticCoord an * where its planet is. How far the moon sits from the planet lives in its ephemeris, which is the * thing that actually positions it.

    */ + + /** + * A moon's orbit, in {@link SystemContent#MOON_UNIT_BLOCKS} units, drawn as a multiple of its + * PARENT's radius. + * + * @param parentRadiusEarths the parent's radius; a body with none stated falls back to one Earth, + * which is what an unstated bulk describes everywhere else in this layer + * @param u the draw, in [0, 1) + */ + private static int moonOrbitUnits(double parentRadiusEarths, double u) { + double radiusBlocks = Math.max(0.05d, parentRadiusEarths) * AstronomicalBodyHelper.EARTH_RADIUS_BLOCKS; + double factor = MOON_MIN_PARENT_RADII + u * (MOON_MAX_PARENT_RADII - MOON_MIN_PARENT_RADII); + long units = Math.round(radiusBlocks * factor / (double) SystemContent.MOON_UNIT_BLOCKS); + return (int) Math.max(1L, Math.min(Integer.MAX_VALUE, units)); + } + private void addMoons(List bodies, long seed, GalacticCoord anchor, GalacticCoord parent, CellFrame parentFrame, int parentOrbit, StellarBody star, int starId, BodyProfile parentProfile) { @@ -705,8 +734,8 @@ private void addMoons(List bodies, long seed, GalacticCoord anchor, ? parentProfile.massEarths() : Math.max(0.05d, parentProfile.gravityPercent() / 100d); for (int j = 1; j <= moons; j++) { - int moonOrbit = MOON_MIN_ORBIT + (int) (CellHash.norm( - CellHash.ofBody(seed, parent, j, SALT_MOONRAD)) * MOON_ORBIT_SPAN); + int moonOrbit = moonOrbitUnits(parentProfile.radiusEarths(), + CellHash.norm(CellHash.ofBody(seed, parent, j, SALT_MOONRAD))); double theta = CellHash.norm(CellHash.ofBody(seed, parent, j, SALT_MOONANG)) * 2d * Math.PI; double periodTicks = AstronomicalBodyHelper.TICKS_PER_DAY * AstronomicalBodyHelper.getMoonOrbitalPeriod(moonOrbit, (float) parentMass); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java index 9e32bd951..8fe0cb9a2 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/GalaxyGenConfig.java @@ -45,7 +45,22 @@ public final class GalaxyGenConfig { */ public static final long DEFAULT_GALAXY_SPACING = UniverseScale.DEFAULT_GALAXY_SPACING_CELLS; - /** Fraction of galaxy cells that actually hold a galaxy, before the cosmic web weights them. */ + /** + * Fraction of galaxy cells that actually hold a galaxy, before the cosmic web weights them. + * + *

    A knob, and deliberately not an observation — unlike its neighbours in this file. The + * star separation, the galaxy radii and the rogue abundance are all measured quantities; this one + * is a chance-per-cube standing in for a number density astronomy states per unit volume, and + * nothing here derives it from a catalogue. It is stated as a knob so the next reader does not + * mistake it for a reading. + * + *

    And it is doing double duty, which is the part worth knowing: half of what it means is + * "structure we have not built". The cosmic web is a deliberate deferral — {@code webDensity} is + * the constant 1 — so the clumping that should come from the web is folded into this single + * uniform chance. Deriving it properly is not a matter of finding a better number; it needs the + * correlated noise the web needs, and until that exists a measured value would be no more honest + * than this one. + */ public static final double DEFAULT_GALAXY_DENSITY = 0.5d; /** A weighted star archetype: a temperature (drives colour) and a size range. */ @@ -371,17 +386,50 @@ private static String digest(String canonical) { /** A sparse, strongly-clustered default galaxy. */ public static GalaxyGenConfig defaults() { - return new GalaxyGenConfig(DEFAULT_MIN_SPACING, 0.35d, DEFAULT_GALAXY_SPACING, - DEFAULT_GALAXY_DENSITY, defaultStarTypes(), defaultGalaxyTypes()); + // The occupancy is READ from the metric rather than repeated here: it is half of what decides + // the mean star separation, and a second copy of it would move the field without moving the + // constant that claims to state where the field is. + return new GalaxyGenConfig(DEFAULT_MIN_SPACING, UniverseScale.DEFAULT_STAR_OCCUPANCY, + DEFAULT_GALAXY_SPACING, DEFAULT_GALAXY_DENSITY, defaultStarTypes(), defaultGalaxyTypes()); } + /** + * The stock star table, weighted by the OBSERVED abundance of each class rather than by a feel for + * how often one should turn up. + * + *

    Weights are per ten thousand systems, from a solar-neighbourhood census BY NUMBER — which is + * the census that matters here, because this table is sampled once per seat. (A census by + * luminosity or by mass gives almost the opposite ordering, and is what makes a blue star feel + * common: it dominates every photograph of the sky while being nearly absent from the volume.)

    + * + * + * + * + * + * + * + * + *
    class, share by number, weight
    M red dwarf~76 %7600
    K orange~12 %1200
    G sun-like~7.6 %760
    F/A white~3.6 %360
    B blue~0.13 %13
    + * + *

    They do not sum to 10 000, and that is correct rather than sloppy: the remaining ~0.7 % is + * white and brown dwarfs, which this table does not model, and O stars at ~3×10-5 % + * are below the resolution of any weight an integer can carry. Weights are relative; a missing + * class is simply absent, not redistributed. + * + *

    What this changed. The previous table read 40/25/20/10/5, i.e. a blue star in one + * system out of twenty against an observed one in seven hundred and sixty — 38× too + * common, against its own comment calling them rare. It flowed downstream too: a star's + * temperature and size set its habitable zone, so an over-bright field made warm orbits commoner + * everywhere. + */ private static List defaultStarTypes() { List l = new ArrayList<>(); - l.add(new StarType(40, 0.6f, 1.0f, 40)); // cool red dwarfs — most common - l.add(new StarType(70, 0.8f, 1.2f, 25)); // orange - l.add(new StarType(100, 0.9f, 1.4f, 20)); // sol-like yellow - l.add(new StarType(150, 1.1f, 1.8f, 10)); // white - l.add(new StarType(220, 1.4f, 2.6f, 5)); // hot blue giants — rare + // temp size band weight (per 10 000 systems, observed) + l.add(new StarType(40, 0.6f, 1.0f, 7600)); // M — red dwarfs, three quarters of every sky + l.add(new StarType(70, 0.8f, 1.2f, 1200)); // K — orange + l.add(new StarType(100, 0.9f, 1.4f, 760)); // G — sun-like + l.add(new StarType(150, 1.1f, 1.8f, 360)); // F/A — white + l.add(new StarType(220, 1.4f, 2.6f, 13)); // B — blue, one system in ~760 return Collections.unmodifiableList(l); } @@ -470,7 +518,14 @@ private static List defaultClusterTypes() { /** Edge of the cube that holds at most one cluster, in light years. */ public static final double CLUSTER_SPACING_LY = 300d; - /** Fraction of those cubes that hold a cluster, before the galaxy's own profile scales it. */ + /** + * Fraction of those cubes that hold a cluster, before the galaxy's own profile scales it. + * + *

    A KNOB, not a reading — the same class as {@link #DEFAULT_GALAXY_DENSITY}: a chance per cube + * standing in for a number density astronomy states per unit volume. Said out loud so the next + * reader does not take it for an observation the way the star separation, the galaxy radii and the + * rogue abundance beside it are. + */ public static final double CLUSTER_DENSITY = 0.35d; /** diff --git a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java index b9866d9ad..c41369f75 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/SystemContent.java @@ -50,6 +50,9 @@ public final class SystemContent { static final long ORBIT_UNIT_BLOCKS = AstronomicalBodyHelper.BLOCKS_PER_ORBIT_UNIT; /** Blocks per unit of a moon's (parent-relative) orbital distance — moons cluster near their planet. */ static final long MOON_UNIT_BLOCKS = 200L; + + /** The floor an authored moon is lifted to, in parent radii — see {@link #moonLawOf}. */ + static final double MOON_MIN_PARENT_RADII = 2.5d; /** Cells kept clear of the super-cell faces when clamping a body cell into its system's box. */ static final int BOX_MARGIN_CELLS = 2; @@ -197,7 +200,17 @@ private static BodyEphemeris orbitLawOf(DimensionProperties planet, StellarBody private static BodyEphemeris moonLawOf(DimensionProperties moon, DimensionProperties parent) { double periodTicks = TICKS_PER_DAY * AstronomicalBodyHelper.getMoonOrbitalPeriod( moon.getOrbitalDist(), (float) parent.getOrbitalMass()); - return BodyEphemeris.orbit(moon.getOrbitalDist(), moon.baseOrbitTheta, moon.orbitalPhi, + // A FLOOR rather than a replacement: an authored pack keeps the spacing it wrote, unless what + // it wrote would put the moon inside its parent. That became possible only when bodies got a + // real radius — an Earth is 25 513 blocks across, so an authored orbit of 100 units (20 000 + // blocks) is under the surface. The pack's intent is kept where it is expressible. + int authored = moon.getOrbitalDist(); + double parentRadiusBlocks = Math.max(0.05d, parent.getRadius()) + * AstronomicalBodyHelper.EARTH_RADIUS_BLOCKS; + long floorUnits = Math.round(parentRadiusBlocks * MOON_MIN_PARENT_RADII + / (double) MOON_UNIT_BLOCKS); + int orbit = (int) Math.max(authored, Math.max(1L, Math.min(Integer.MAX_VALUE, floorUnits))); + return BodyEphemeris.orbit(orbit, moon.baseOrbitTheta, moon.orbitalPhi, moon.isRetrograde, periodTicks, MOON_UNIT_BLOCKS); } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java index 418653c9c..2c76c57e4 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java @@ -614,6 +614,28 @@ public boolean forgetName(int dimId) { * Server-side convenience for the dimension lifecycle: forget {@code dimId}'s recorded name on * whatever registry is reachable. A no-op with no server (a client, a unit test). */ + /** + * The bodies standing in {@code cell}, resolved through the running server's registry — for + * callers that hold an ADDRESS and no way to reach a registry, which is most of the space layer's + * entry path. An empty list when there is no server, no registry, or nothing there. + */ + public static List bodiesAtOnServer(GalacticCoord cell) { + if (cell == null) { + return Collections.emptyList(); + } + UniverseRegistry reg; + try { + reg = get(net.minecraftforge.fml.common.FMLCommonHandler.instance() + .getMinecraftServerInstance()); + } catch (Throwable noServer) { + // No Forge bootstrap at all — a pure unit context. "There is no server, so there is + // nothing standing in that cell" is the honest answer here and the caller's own fallback + // (the flat ring) is the right behaviour, so this is not swallowed error handling. + return Collections.emptyList(); + } + return (reg == null) ? Collections.emptyList() : reg.systemBodiesAt(cell); + } + public static void forgetNameOnServer(int dimId) { UniverseRegistry reg = get(net.minecraftforge.fml.common.FMLCommonHandler.instance() .getMinecraftServerInstance()); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java index ba496977c..ffbb11757 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseScale.java @@ -37,12 +37,35 @@ public final class UniverseScale { /** - * Mean distance between neighbouring star seats, in light years. Real stellar neighbourhoods run - * 4–5 light years between neighbours; the lattice is stratified rather than Poisson, so the - * mean neighbour distance it produces comes out somewhat above this edge. + * Mean distance between neighbouring star seats, in light years — and the lattice is now built to + * PRODUCE it rather than to use it as a cube edge. + * + *

    4.23 is the observed figure for a solar neighbourhood, and it is the quantity this layer + * actually means; the cube edge is machinery underneath it. Until 2026-08-19 this number was + * consumed directly as {@link #DEFAULT_SPACING_CELLS}, i.e. as the EDGE, which is a different + * quantity: a cube of edge {@code e} occupied with probability {@code p} puts its neighbours + * {@code e / p^(1/3)} apart, so the field stood 6.0 ly apart at the shipped occupancy while + * this constant said 4.23. Measured on the shipped generator before the fix: 4913 territories + * around the origin seated 1574 systems, occupancy 0.320, mean separation 6.18 ly — 42 % above + * what the name promised, and the javadoc attributed the excess to the lattice being stratified + * when the dominant term was the occupancy.

    + * + *

    See {@link #DEFAULT_STAR_OCCUPANCY} for the knob that closes the gap, and + * {@link #DEFAULT_SPACING_CELLS} for the edge that now follows from both.

    */ public static final double MEAN_STAR_SEPARATION_LY = 4.23d; + /** + * What fraction of star territories hold a system at a galaxy's densest point — the lattice's fill, + * and a balance knob rather than an observation. + * + *

    It lives here beside the separation because the two together decide the edge, and a knob that + * silently changes a measured quantity belongs next to the quantity it changes. A pack may still + * override the occupancy through {@code }; what it cannot do is move the + * separation without saying so, because the separation is what the edge is derived from.

    + */ + public static final double DEFAULT_STAR_OCCUPANCY = 0.35d; + /** * The guaranteed clear space around a system, in AU: two stars never stand closer than this, * however the lattice falls. Four times the widest binary the star model describes, so a lattice @@ -72,14 +95,33 @@ public final class UniverseScale { public static final long SEAT_MARGIN_CELLS = cellsForOrbitUnits(MAX_NAMED_ORBIT_UNITS); /** - * Default edge of the cube that holds at most one system, in cells. Derived from - * {@link #MEAN_STAR_SEPARATION_LY}; a balance knob, overridable from the universe generator's - * configuration, and never a contract. + * Default edge of the cube that holds at most one system, in cells — machinery, derived from the + * two quantities that mean something: the separation the field should show and the fraction of + * territories that hold anything. + * + *

    {@code edge = separation × occupancy^(1/3)}, the inverse of the relation in + * {@link #MEAN_STAR_SEPARATION_LY}: a sparser lattice needs a smaller cube to put its neighbours the + * same distance apart. At the shipped 4.23 ly and 0.35 that is 2.98 ly of edge, down from the 4.23 + * this used to take verbatim — a finer partition, and the field lands where the constant says.

    + * + *

    The clear space a seat needs is unaffected and remains far below the new edge: + * {@link #SEPARATION_FLOOR_AU} is 10 000 AU = 0.158 ly, i.e. about 5 % of this edge rather than the + * 3.7 % it was. A balance knob, overridable from the generator's configuration, never a contract.

    */ public static final int DEFAULT_SPACING_CELLS = (int) Math.min(Integer.MAX_VALUE, - Math.max(1L, Math.round(MEAN_STAR_SEPARATION_LY + Math.max(1L, Math.round(MEAN_STAR_SEPARATION_LY * Math.cbrt(DEFAULT_STAR_OCCUPANCY) * AstronomicalBodyHelper.BLOCKS_PER_LIGHT_YEAR / (double) GalacticCoord.CELL))); + /** + * The mean separation a lattice of {@code edgeCells} at occupancy {@code occupancy} actually + * produces, in light years — the relation stated once, so a caller can ask instead of re-deriving. + */ + public static double meanSeparationLy(long edgeCells, double occupancy) { + double edgeLy = lightYearsForCells(edgeCells); + double p = Math.min(1d, Math.max(1e-9d, occupancy)); + return edgeLy / Math.cbrt(p); + } + // ─── The galaxy lattice ──────────────────────────────────────────────────── // One level up, and the same scheme: a cube that holds at most one galaxy, and a galaxy seated // inside it. What is stated here is a REFERENCE SIZE and a RATIO; the separation follows from diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index 52acbf3b6..0da7751ed 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -716,6 +716,143 @@ private static int majorBodies(List bodies) { return n; } + // ── a body's size is what its neighbours are measured against ───────────── + + @Test + public void aMoonStandsOutsideItsParent() { + // The defect this closes: a moon's orbit was an absolute length (4 000–26 000 blocks) chosen + // when a planet had no radius. Bodies then got one — an Earth is 25 513 blocks across and a + // Jupiter 280 643 — so essentially every moon was seated INSIDE its parent, and a giant's by an + // order of magnitude. + // + // The assertion is geometric and takes no number from production: at a fixed tick, the + // separation between a moon and its parent must exceed the parent's own radius. A test that + // pinned "2.5 radii" would pin the tuning; this pins that a moon is a thing you can see from + // the world it goes round. + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(defaultsCfg()); + long tick = 12_345L; + int checkedMoons = 0; + int checkedParents = 0; + + for (long seed = 1L; seed <= 6L; seed++) { + for (GalacticCoord anchor : anchors(gen, seed, SPACING, 2)) { + List bodies = gen.bodiesFor(seed, anchor); + for (SystemBody moon : bodies) { + if (moon.kind() != SystemBodyKind.MOON) { + continue; + } + SystemBody parent = null; + for (SystemBody candidate : bodies) { + if (candidate != moon && candidate.definesFrame() + && candidate.name().cellKey().equals(moon.name().cellKey())) { + parent = candidate; + break; + } + } + if (parent == null || parent.radiusEarths() <= 0d) { + continue; + } + checkedParents++; + zmaster587.advancedRocketry.space.BlockDelta m = moon.inCellOffsetAt(tick); + zmaster587.advancedRocketry.space.BlockDelta p = parent.inCellOffsetAt(tick); + double ddx = (double) (m.dx() - p.dx()); + double ddy = (double) (m.dy() - p.dy()); + double ddz = (double) (m.dz() - p.dz()); + double separation = Math.sqrt(ddx * ddx + ddy * ddy + ddz * ddz); + double parentRadiusBlocks = + parent.radiusEarths() * AstronomicalBodyHelper.EARTH_RADIUS_BLOCKS; + assertTrue("a moon must stand outside the world it orbits: separation " + + Math.round(separation) + " blocks against a parent radius of " + + Math.round(parentRadiusBlocks) + " (" + parent.kind() + " at " + + parent.name().cellKey() + ")", + separation > parentRadiusBlocks); + checkedMoons++; + } + } + } + System.out.println("checked " + checkedMoons + " moons against " + checkedParents + " parents"); + assertTrue("arrangement: the sweep must find moons to check, or this proves nothing", + checkedMoons >= 10); + } + + // ── the constants say what they mean ────────────────────────────────────── + + @Test + public void theFieldStandsAsFarApartAsTheConstantSaysItDoes() { + // MEAN_STAR_SEPARATION_LY is a MEASURED astronomical quantity, so the lattice owes it as an + // OUTPUT, not as an input it happens to be spelled with. It used to be consumed as the cube + // edge, which is a different quantity: a cube of edge e filled with probability p puts its + // neighbours e/p^(1/3) apart, so the field stood 42 % further apart than the constant claimed + // and nothing said so. This test is the thing that would have said so. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); + + int span = 8; // a 17-cube of territories: enough seats for the ratio to settle + int territories = 0; + int seated = 0; + for (int i = -span; i <= span; i++) { + for (int j = -span; j <= span; j++) { + for (int k = -span; k <= span; k++) { + territories++; + // STARS, not systems. The unbound population seats a rogue world in essentially + // every territory a star left empty, so counting systems measures occupancy 1.0 + // and says nothing about how far apart the STARS stand — which is the quantity + // MEAN_STAR_SEPARATION_LY is about. (Measured here first: 4913 of 4913.) + // Through the ANCHOR: systemAt answers on the seat cell alone, and a territory's + // corner is not its seat. + Optional anchor = gen.anchorAt(SEED, + cell((long) i * config.minSpacing, (long) j * config.minSpacing, + (long) k * config.minSpacing)); + if (!anchor.isPresent()) { + continue; + } + Optional here = gen.systemAt(SEED, anchor.get()); + if (here.isPresent() && here.get().star().isPresent()) { + seated++; + } + } + } + } + assertTrue("arrangement: the sweep must find a populated star field", seated > territories / 10); + + double occupancy = seated / (double) territories; + double separation = UniverseScale.meanSeparationLy(config.minSpacing, occupancy); + double claimed = UniverseScale.MEAN_STAR_SEPARATION_LY; + System.out.println("swept " + territories + " territories, seated " + seated + + " (occupancy " + occupancy + ") -> mean separation " + separation + " ly against " + + claimed); + + // A band, not a number: the galaxy's own profile scales the occupancy even at the centre, so + // the produced separation sits a little above the bare lattice's. What is pinned is that the + // constant DESCRIBES the field — a return to consuming it as an edge lands ~42 % out and red. + assertTrue("the field must stand about as far apart as MEAN_STAR_SEPARATION_LY claims: " + + separation + " ly against " + claimed, + separation > claimed * 0.85d && separation < claimed * 1.2d); + } + + @Test + public void aBlueStarIsAFindAndARedDwarfIsTheSky() { + // The weights are an observed census by NUMBER, so what they owe is the ORDER OF MAGNITUDE + // between classes, not any particular value. They read 40/25/20/10/5 before — a blue star in + // one system out of twenty, against an observed one in seven hundred and sixty, while the + // table's own comment called them rare. + List table = GalaxyGenConfig.defaults().starTypes; + assertEquals("arrangement: the stock table is the five-class one", 5, table.size()); + + for (int i = 1; i < table.size(); i++) { + assertTrue("a hotter class must never be commoner than a cooler one: " + + table.get(i - 1).temperature + " weighted " + table.get(i - 1).weight + + " against " + table.get(i).temperature + " weighted " + table.get(i).weight, + table.get(i).weight < table.get(i - 1).weight); + } + + GalaxyGenConfig.StarType coolest = table.get(0); + GalaxyGenConfig.StarType hottest = table.get(table.size() - 1); + assertTrue("a red dwarf must outnumber a blue star by at least two orders, as observed: " + + coolest.weight + " against " + hottest.weight, + coolest.weight >= hottest.weight * 100); + } + // ── the derivation is part of the world model ───────────────────────────── /** A derivation that differs from version 1 in one law, and delegates the rest. */ @@ -1056,13 +1193,22 @@ private static void renderSystem(List out, ClusteredGalaxyGenerator g, l } } + /** One fixed instant, so an orbiting body has a POSITION the corpus can compare. */ + private static final long OBSERVED_TICK = 12_345L; + private static String renderBody(GalacticCoord anchor, SystemBody body) { + // The in-cell OFFSET is rendered, and it has to be: a moon carries its PARENT's orbital + // distance in orbitalDistance() and stands in its parent's cell, so identity and radius + // alone leave a moon's position entirely unobserved — the corpus stayed byte-identical + // across a change that moved every moon in the universe. + zmaster587.advancedRocketry.space.BlockDelta at = body.inCellOffsetAt(OBSERVED_TICK); return " body " + anchor.cellKey() + ' ' + body.name().cellKey() + " kind=" + body.kind() + " orbit=" + body.orbitalDistance() + " radius=" + Double.toString(body.radiusEarths()) + " starId=" + body.starId() - + " frame=" + body.definesFrame(); + + " frame=" + body.definesFrame() + + " at=" + at.dx() + ',' + at.dy() + ',' + at.dz(); } private static String renderDerivation(ClusteredGalaxyGenerator g, long seed, diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/DescentShellTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/DescentShellTest.java index d105b30e2..1bac54a88 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/DescentShellTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/DescentShellTest.java @@ -98,4 +98,47 @@ public void theRangeIsShorterThanTheDistanceByTheWholeShell() { assertEquals("the readout must differ from the centre distance by exactly the shell", R, d - DescentShell.distanceToShell(d, R), 0d); } + + // ── the shell is a property of the BODY ─────────────────────────────────── + + /** A body of {@code radiusEarths}, standing at the origin, with nothing else stated. */ + private static zmaster587.advancedRocketry.universe.SystemBody sized(double radiusEarths) { + return zmaster587.advancedRocketry.universe.SystemBody.fixedAt( + zmaster587.advancedRocketry.space.GalacticCoord.ORIGIN, + zmaster587.advancedRocketry.universe.SystemBodyKind.PLANET, + zmaster587.advancedRocketry.api.Constants.INVALID_PLANET, 0) + .withRadius(radiusEarths); + } + + @Test + public void aShellStandsOutsideTheWorldItBounds() { + // The defect this closes: radiusAround ignored its argument and returned a flat 512 blocks, + // chosen when a body had no size. Against the radii that now exist that is 1/50 of an Earth + // and 1/548 of a Jupiter — the surface a descent fires at lay deep inside the world it belongs + // to. Every size the generator can produce is checked, not one convenient case. + double[] radii = {0.1d, 0.5d, 1d, 2.5d, 11d, 30d}; + for (double r : radii) { + zmaster587.advancedRocketry.universe.SystemBody body = sized(r); + long shell = zmaster587.advancedRocketry.space.DescentShell.radiusAround(body); + double surface = r * zmaster587.advancedRocketry.util.AstronomicalBodyHelper.EARTH_RADIUS_BLOCKS; + + assertTrue("a descent shell must stand OUTSIDE the body it bounds: " + shell + + " against a surface at " + Math.round(surface) + " (r=" + r + ")", + shell > surface); + assertTrue("and an entering ship must spawn outside that shell, or it arrives already " + + "inside the trigger it is flying towards (r=" + r + ")", + zmaster587.advancedRocketry.space.ShipEntryController.entryRingAround(body) > shell); + } + } + + @Test + public void aBodyWithNoSizeKeepsTheFlatProximityRadius() { + // A belt or a station slot is not a sphere and has no surface to stand above, so the constant + // is the right answer there rather than a fallback — and a body whose atmosphere would be + // thinner than a ship's manoeuvring scale keeps it too. + assertEquals(zmaster587.advancedRocketry.space.ShipEntryController.DESCENT_RADIUS_BLOCKS, + zmaster587.advancedRocketry.space.DescentShell.radiusAround(sized(0d))); + assertEquals(zmaster587.advancedRocketry.space.ShipEntryController.DESCENT_RADIUS_BLOCKS, + zmaster587.advancedRocketry.space.DescentShell.radiusAround(null)); + } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java index 5d7a53613..b4dde7580 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/DriveLadderTest.java @@ -252,10 +252,20 @@ public void theInterstellarBandIsWhatTheGeneratorActuallyProduces() { long stride = 4L * GalaxyGenConfig.DEFAULT_MIN_SPACING; List legs = new ArrayList<>(); for (long seed = 1L; seed <= 20L; seed++) { - Map found = gen.systemsInRegion(seed, + Map all = gen.systemsInRegion(seed, cell(-stride, -stride, -stride), cell(stride, stride, stride)); - GalacticCoord home = nearestTo(found.keySet(), cell(0L, 0L, 0L)); - GalacticCoord neighbour = home == null ? null : nearestTo(found.keySet(), home); + // STAR systems only. Since the void was populated, an unbound world sits in essentially every + // territory the stars left empty, so "the nearest system" stopped meaning "the nearest star" — + // and a leg measured over all seats is the lattice EDGE rather than the separation this band is + // declared against. A jump is aimed at something a telescope found, which is a star. + java.util.Set found = new java.util.LinkedHashSet<>(); + for (Map.Entry e : all.entrySet()) { + if (e.getValue().star().isPresent()) { + found.add(e.getKey()); + } + } + GalacticCoord home = nearestTo(found, cell(0L, 0L, 0L)); + GalacticCoord neighbour = home == null ? null : nearestTo(found, home); if (neighbour == null) { continue; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java index ccb88269a..15278baa8 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/InterstellarLegDistanceTest.java @@ -193,11 +193,21 @@ public void theMeasuredBandMatchesTheArithmeticItIsDerivedFrom() { /** The distance from the system nearest the origin to ITS nearest neighbour, in light years. */ private static Double nearestNeighbourLightYears(ClusteredGalaxyGenerator gen, long seed) { - Map found = gen.systemsInRegion(seed, + Map all = gen.systemsInRegion(seed, cell(-SEARCH_RADIUS_CELLS, -SEARCH_RADIUS_CELLS, -SEARCH_RADIUS_CELLS), cell(SEARCH_RADIUS_CELLS, SEARCH_RADIUS_CELLS, SEARCH_RADIUS_CELLS)); - GalacticCoord home = nearestTo(found.keySet(), cell(0L, 0L, 0L)); - GalacticCoord neighbour = home == null ? null : nearestTo(found.keySet(), home); + // STAR systems only. Since the void was populated an unbound world sits in nearly every + // territory the stars left empty, so a leg measured over all seats is the lattice EDGE and not + // the star separation this band is declared against. A jump is aimed at what a telescope + // found, which is a star. + java.util.Set found = new java.util.LinkedHashSet<>(); + for (Map.Entry e : all.entrySet()) { + if (e.getValue().star().isPresent()) { + found.add(e.getKey()); + } + } + GalacticCoord home = nearestTo(found, cell(0L, 0L, 0L)); + GalacticCoord neighbour = home == null ? null : nearestTo(found, home); if (neighbour == null) { return null; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java index 40e96a79c..e124c5f58 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java @@ -480,32 +480,42 @@ private static GalaxyGenConfig retuned() { public void aSystemAScanReportedIsFrozenAgainstALaterRetune() { // The promise the whole schema-versioning rests on: what the player has SEEN stops moving. // A survey answers out of the derivation, so without a pin the system on his crystal is a - // function of the pack's current knobs — and he finds that out by flying there. + // function of the world's parameters — and he finds that out by flying there. + // + // The "different universe" is a different SEED rather than a retuned config, and that choice is + // the point. Two earlier forms of this test used a config retune and both went vacuous without + // saying so: the first picked the ORIGIN, whose territory carries lattice index (0,0,0) whatever + // the edge is, and the second picked an anchor the retune happened not to move. A seed change + // re-derives everything by construction, so the discriminator below cannot quietly stop + // discriminating. GalaxyGenConfig config = GalaxyGenConfig.defaults(); UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(config)); UniverseRegistry.setStarLookup(TelescopeRegionScanTest::star); UniverseRegistry registry = new UniverseRegistry(); registry.bindWorldSeed(0xC0FFEEL); - GalacticCoord looked = cell(0, 0, 0); - GalacticCoord neverLooked = cell(3 * STEP, 0, 0); - GalacticCoord lookedAnchor = registry.anchorForCell(looked).orElse(null); - GalacticCoord otherAnchor = registry.anchorForCell(neverLooked).orElse(null); - assertNotNull("arrangement: the looked-at cell must hold a system", lookedAnchor); - assertNotNull("arrangement: the control cell must hold a system", otherAnchor); - String lookedBefore = describe(registry, lookedAnchor); - String otherBefore = describe(registry, otherAnchor); + GalacticCoord looked = cell(7 * STEP, 3 * STEP, -5 * STEP); + GalacticCoord anchor = registry.anchorForCell(looked).orElse(null); + assertNotNull("arrangement: the looked-at cell must hold a system", anchor); + String before = describe(registry, anchor); CrystalMemory crystal = new CrystalMemory(); assertTrue("arrangement: the look must report something", TelescopeScan.resolveCell(registry, looked, crystal, 7_000L, dimId -> "Body-" + dimId) > 0); - UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(retuned())); + // A different universe under the same registry. + registry.bindWorldSeed(0xDEADBEEFL); - assertNotEquals("arrangement: the retune must actually move an untouched system, or this test " - + "proves nothing", otherBefore, describe(registry, otherAnchor)); - assertEquals("a system a telescope reported must survive a retune of the universe it was " - + "derived from", lookedBefore, describe(registry, lookedAnchor)); + assertEquals("a system a telescope reported must survive a change to the universe it was " + + "derived from", before, describe(registry, anchor)); + + // The discriminator: the new universe must genuinely describe something else at that anchor, + // or the assertion above would hold with no pin at all. + String derivedNow = new ClusteredGalaxyGenerator(config).systemAt(0xDEADBEEFL, anchor) + .map(sys -> sys.systemId() + "/" + sys.primaryKind() + "/" + sys.name()) + .orElse("none"); + assertNotEquals("arrangement: the new seed must derive something else at this anchor, or the " + + "freeze is untested", before.substring(0, before.indexOf('[')), derivedNow); } @Test diff --git a/src/test/resources/universe/golden-corpus-v1.txt b/src/test/resources/universe/golden-corpus-v1.txt index a72819f9e..d5f220d90 100644 --- a/src/test/resources/universe/golden-corpus-v1.txt +++ b/src/test/resources/universe/golden-corpus-v1.txt @@ -1,6 +1,6 @@ # universe golden corpus - schema 0 -config d838c54e8bdec274 -scale spacingCells=5002361 galaxySpacingCells=2956478272682 seatMarginCells=93499 +config 54856f457186f8ee +scale spacingCells=3525313 galaxySpacingCells=2956478272682 seatMarginCells=93499 scale ly=0.1 cells=118260 backLy=0.10000073490540135 scale ly=1.0 cells=1182592 backLy=1.0000005842486757 scale ly=4.23 cells=5002362 backLy=4.230000644874457 @@ -10,1502 +10,1755 @@ cosmology tick=0 scaleFactor=1.0 cosmology tick=24000 scaleFactor=1.0000000000014915 cosmology tick=24000000 scaleFactor=1.0000000014914552 seed 1 systems=27 - body -146732_3144538_9058898 -146732_3144538_9058898 kind=MOON orbit=0 radius=0.9877675872558382 starId=-478929905 frame=false - body -146732_3144538_9058898 -146732_3144538_9058898 kind=ROGUE_PLANET orbit=0 radius=0.365137001300595 starId=-478929905 frame=true - body -1990844_4373138_-3056240 -1990792_4373137_-3056198 kind=MOON orbit=355 radius=0.28953652111498945 starId=-1524375109 frame=false - body -1990844_4373138_-3056240 -1990792_4373137_-3056198 kind=PLANET orbit=355 radius=1.2959853165262225 starId=-1524375109 frame=true - body -1990844_4373138_-3056240 -1990836_4373138_-3056240 kind=MOON orbit=45 radius=0.4447060804848286 starId=-1524375109 frame=false - body -1990844_4373138_-3056240 -1990836_4373138_-3056240 kind=PLANET orbit=45 radius=2.0020815425545506 starId=-1524375109 frame=true - body -1990844_4373138_-3056240 -1990844_4373138_-3056240 kind=STAR orbit=0 radius=0.0 starId=-1524375109 frame=true - body -1990844_4373138_-3056240 -1990845_4373138_-3056241 kind=MOON orbit=10 radius=0.22470924311305454 starId=-1524375109 frame=false - body -1990844_4373138_-3056240 -1990845_4373138_-3056241 kind=MOON orbit=10 radius=0.6117166796218898 starId=-1524375109 frame=false - body -1990844_4373138_-3056240 -1990845_4373138_-3056241 kind=PLANET orbit=10 radius=0.3898083310932754 starId=-1524375109 frame=true - body -1990844_4373138_-3056240 -1990851_4373138_-3056227 kind=ASTEROID_BELT orbit=80 radius=0.0 starId=-1524375109 frame=true - body -1990844_4373138_-3056240 -1990857_4373137_-3056263 kind=GAS_GIANT orbit=144 radius=6.336033623503909 starId=-1524375109 frame=true - body -1990844_4373138_-3056240 -1990868_4373135_-3056136 kind=ASTEROID_BELT orbit=568 radius=0.0 starId=-1524375109 frame=true - body -2174817_-2291967_261255 -2174817_-2291967_261255 kind=ROGUE_PLANET orbit=0 radius=1.3479949218897396 starId=-1472462165 frame=true - body -3680127_6497425_-2974555 -3680127_6497425_-2974555 kind=MOON orbit=0 radius=2.3820978624799545 starId=-1983124585 frame=false - body -3680127_6497425_-2974555 -3680127_6497425_-2974555 kind=ROGUE_PLANET orbit=0 radius=1.7790054869434075 starId=-1983124585 frame=true - body -3867498_449288_4118676 -3867498_449288_4118676 kind=ROGUE_PLANET orbit=0 radius=0.2746107364620147 starId=-1943963613 frame=true - body -4373024_7511005_1816766 -4370934_7510969_1815501 kind=ASTEROID_BELT orbit=13067 radius=0.0 starId=-110594437 frame=true - body -4373024_7511005_1816766 -4372159_7511003_1818025 kind=PLANET orbit=8167 radius=1.3472896720527072 starId=-110594437 frame=true - body -4373024_7511005_1816766 -4372915_7511005_1817033 kind=STAR orbit=1543 radius=104.0277603185177 starId=-110594438 frame=true - body -4373024_7511005_1816766 -4372992_7511006_1816751 kind=ASTEROID_BELT orbit=189 radius=0.0 starId=-110594437 frame=true - body -4373024_7511005_1816766 -4373006_7511005_1816753 kind=PLANET orbit=119 radius=0.6040809505464881 starId=-110594437 frame=true - body -4373024_7511005_1816766 -4373024_7511005_1816766 kind=STAR orbit=0 radius=0.0 starId=-110594437 frame=true - body -4373024_7511005_1816766 -4373066_7511005_1816718 kind=GAS_GIANT orbit=341 radius=3.461531189557112 starId=-110594437 frame=true - body -4373024_7511005_1816766 -4373066_7511005_1816718 kind=MOON orbit=341 radius=0.20073377523039576 starId=-110594437 frame=false - body -4373024_7511005_1816766 -4373066_7511005_1816718 kind=MOON orbit=341 radius=0.24686627491590246 starId=-110594437 frame=false - body -4752099_9055058_8335985 -4752099_9055058_8335985 kind=MOON orbit=0 radius=1.1667301892464876 starId=-392839469 frame=false - body -4752099_9055058_8335985 -4752099_9055058_8335985 kind=ROGUE_PLANET orbit=0 radius=0.24557082150399945 starId=-392839469 frame=true - body -4890357_-2814414_6240914 -4890357_-2814414_6240914 kind=ROGUE_PLANET orbit=0 radius=1.6732860210523022 starId=-759681393 frame=true - body -670991_-4121660_-2164613 -670621_-4121674_-2164509 kind=ASTEROID_BELT orbit=2054 radius=0.0 starId=-262321917 frame=true - body -670991_-4121660_-2164613 -670902_-4121657_-2164531 kind=GAS_GIANT orbit=646 radius=7.1517935902824386 starId=-262321917 frame=true - body -670991_-4121660_-2164613 -670986_-4121661_-2164629 kind=ASTEROID_BELT orbit=90 radius=0.0 starId=-262321917 frame=true - body -670991_-4121660_-2164613 -670989_-4121660_-2164613 kind=PLANET orbit=11 radius=2.341480090625241 starId=-262321917 frame=true - body -670991_-4121660_-2164613 -670991_-4121660_-2164613 kind=STAR orbit=0 radius=0.0 starId=-262321917 frame=true - body -670991_-4121660_-2164613 -670996_-4121660_-2164616 kind=MOON orbit=31 radius=0.4448751111389811 starId=-262321917 frame=false - body -670991_-4121660_-2164613 -670996_-4121660_-2164616 kind=MOON orbit=31 radius=0.4933305978587888 starId=-262321917 frame=false - body -670991_-4121660_-2164613 -670996_-4121660_-2164616 kind=PLANET orbit=31 radius=0.27867397404199995 starId=-262321917 frame=true - body -670991_-4121660_-2164613 -671002_-4121659_-2164570 kind=PLANET orbit=238 radius=1.8533917606053845 starId=-262321917 frame=true - body -670991_-4121660_-2164613 -671003_-4121660_-2164619 kind=MOON orbit=71 radius=0.20106576597243764 starId=-262321917 frame=false - body -670991_-4121660_-2164613 -671003_-4121660_-2164619 kind=MOON orbit=71 radius=0.5236136532220199 starId=-262321917 frame=false - body -670991_-4121660_-2164613 -671003_-4121660_-2164619 kind=PLANET orbit=71 radius=0.5699188917970125 starId=-262321917 frame=true - body -670991_-4121660_-2164613 -671008_-4121660_-2164588 kind=GAS_GIANT orbit=162 radius=4.148350959171349 starId=-262321917 frame=true - body -670991_-4121660_-2164613 -671008_-4121660_-2164588 kind=MOON orbit=162 radius=0.28685003312625307 starId=-262321917 frame=false - body -670991_-4121660_-2164613 -671165_-4121668_-2164448 kind=PLANET orbit=1284 radius=1.518213839049826 starId=-262321917 frame=true - body 1194346_-4530025_6629562 1191593_-4530197_6633767 kind=MOON orbit=26895 radius=0.2780300806379663 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1191593_-4530197_6633767 kind=MOON orbit=26895 radius=0.3160862042273091 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1191593_-4530197_6633767 kind=PLANET orbit=26895 radius=0.3710815068256704 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1194117_-4530031_6629817 kind=GAS_GIANT orbit=1835 radius=3.260041612564983 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1194117_-4530031_6629817 kind=MOON orbit=1835 radius=0.2002745486322834 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1194162_-4530030_6629610 kind=ASTEROID_BELT orbit=1019 radius=0.0 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1194189_-4530017_6629650 kind=MOON orbit=961 radius=0.4493509992896797 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1194189_-4530017_6629650 kind=PLANET orbit=961 radius=1.910283521314024 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1194309_-4530025_6629559 kind=PLANET orbit=197 radius=0.5175670411566737 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1194313_-4530027_6629512 kind=MOON orbit=323 radius=0.2644569179326812 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1194313_-4530027_6629512 kind=MOON orbit=323 radius=0.28093495512902344 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1194313_-4530027_6629512 kind=PLANET orbit=323 radius=1.986963906941915 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1194346_-4530025_6629562 kind=STAR orbit=0 radius=0.0 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1194351_-4530025_6629565 kind=STAR orbit=31 radius=89.65360039293766 starId=-962147134 frame=true - body 1194346_-4530025_6629562 1194475_-4530022_6629580 kind=PLANET orbit=695 radius=1.4999718538392695 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1194906_-4530040_6629503 kind=GAS_GIANT orbit=3014 radius=4.943021985795619 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1194906_-4530040_6629503 kind=MOON orbit=3014 radius=0.21118755057036187 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1194906_-4530040_6629503 kind=MOON orbit=3014 radius=0.6653742939458489 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1195455_-4529982_6629794 kind=MOON orbit=6065 radius=0.25884471688542643 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1195455_-4529982_6629794 kind=MOON orbit=6065 radius=0.680236793586125 starId=-962147133 frame=false - body 1194346_-4530025_6629562 1195455_-4529982_6629794 kind=PLANET orbit=6065 radius=1.081029895238314 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1195499_-4530033_6628603 kind=PLANET orbit=8022 radius=0.4745120039726671 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1195668_-4530126_6632475 kind=GAS_GIANT orbit=17115 radius=7.003259256804836 starId=-962147133 frame=true - body 1194346_-4530025_6629562 1196104_-4530132_6637414 kind=ASTEROID_BELT orbit=43032 radius=0.0 starId=-962147133 frame=true - body 1756923_-1971171_998832 1756304_-1971195_999471 kind=MOON orbit=4759 radius=0.3850177207349733 starId=-1811991613 frame=false - body 1756923_-1971171_998832 1756304_-1971195_999471 kind=MOON orbit=4759 radius=0.4933377071413044 starId=-1811991613 frame=false - body 1756923_-1971171_998832 1756304_-1971195_999471 kind=PLANET orbit=4759 radius=0.36539458876974434 starId=-1811991613 frame=true - body 1756923_-1971171_998832 1756802_-1971224_997414 kind=ASTEROID_BELT orbit=7614 radius=0.0 starId=-1811991613 frame=true - body 1756923_-1971171_998832 1756895_-1971172_998845 kind=PLANET orbit=163 radius=0.2058604344637433 starId=-1811991613 frame=true - body 1756923_-1971171_998832 1756923_-1971171_998832 kind=STAR orbit=0 radius=0.0 starId=-1811991613 frame=true - body 1756923_-1971171_998832 1756927_-1971170_998853 kind=PLANET orbit=114 radius=0.6445989983146653 starId=-1811991613 frame=true - body 1756923_-1971171_998832 1756928_-1971171_998828 kind=MOON orbit=35 radius=0.48316917319884634 starId=-1811991613 frame=false - body 1756923_-1971171_998832 1756928_-1971171_998828 kind=PLANET orbit=35 radius=0.6410672902404898 starId=-1811991613 frame=true - body 1756923_-1971171_998832 1757015_-1971171_998790 kind=STAR orbit=540 radius=118.307769895792 starId=-1811991614 frame=true - body 1756923_-1971171_998832 1757193_-1971162_999102 kind=PLANET orbit=2041 radius=1.3665356678264369 starId=-1811991613 frame=true - body 1836710_7713193_-3440196 1836710_7713193_-3440196 kind=ROGUE_PLANET orbit=0 radius=0.9935421843295233 starId=-1441927509 frame=true - body 2469282_4416743_-723947 2469276_4416743_-723937 kind=ASTEROID_BELT orbit=65 radius=0.0 starId=-776084721 frame=true - body 2469282_4416743_-723947 2469282_4416743_-723942 kind=PLANET orbit=26 radius=0.8567827527241039 starId=-776084721 frame=true - body 2469282_4416743_-723947 2469282_4416743_-723947 kind=STAR orbit=0 radius=0.0 starId=-776084721 frame=true - body 2469282_4416743_-723947 2469283_4416743_-723947 kind=PLANET orbit=7 radius=1.6637654823213972 starId=-776084721 frame=true - body 2469282_4416743_-723947 2469303_4416743_-723951 kind=GAS_GIANT orbit=117 radius=8.252127224356407 starId=-776084721 frame=true - body 2469282_4416743_-723947 2469310_4416743_-723868 kind=ASTEROID_BELT orbit=446 radius=0.0 starId=-776084721 frame=true - body 2469282_4416743_-723947 2469310_4416744_-723991 kind=PLANET orbit=279 radius=0.3860558158888115 starId=-776084721 frame=true - body 2948486_1502429_3559523 2948486_1502429_3559523 kind=ROGUE_PLANET orbit=0 radius=2.4054665038276375 starId=-875095761 frame=true - body 3510787_4170368_5776814 3510787_4170368_5776814 kind=MOON orbit=0 radius=1.8026104261664744 starId=-205539377 frame=false - body 3510787_4170368_5776814 3510787_4170368_5776814 kind=ROGUE_PLANET orbit=0 radius=1.1577006008040243 starId=-205539377 frame=true - body 3709337_6524042_9099103 3709202_6524034_9098955 kind=ASTEROID_BELT orbit=1073 radius=0.0 starId=-1795014233 frame=true - body 3709337_6524042_9099103 3709286_6524045_9099143 kind=MOON orbit=348 radius=0.24336564597805202 starId=-1795014233 frame=false - body 3709337_6524042_9099103 3709286_6524045_9099143 kind=MOON orbit=348 radius=0.24796229063275702 starId=-1795014233 frame=false - body 3709337_6524042_9099103 3709286_6524045_9099143 kind=PLANET orbit=348 radius=0.20215208272759683 starId=-1795014233 frame=true - body 3709337_6524042_9099103 3709337_6524042_9099103 kind=STAR orbit=0 radius=0.0 starId=-1795014233 frame=true - body 3709337_6524042_9099103 3709337_6524042_9099104 kind=PLANET orbit=8 radius=0.2024093892049266 starId=-1795014233 frame=true - body 3709337_6524042_9099103 3709343_6524042_9099110 kind=STAR orbit=52 radius=69.71192023336887 starId=-1795014234 frame=true - body 3709337_6524042_9099103 3709393_6524040_9098991 kind=MOON orbit=671 radius=0.21707797232613227 starId=-1795014233 frame=false - body 3709337_6524042_9099103 3709393_6524040_9098991 kind=MOON orbit=671 radius=0.30680587463262243 starId=-1795014233 frame=false - body 3709337_6524042_9099103 3709393_6524040_9098991 kind=PLANET orbit=671 radius=0.317794798051077 starId=-1795014233 frame=true - body 4213602_-4440402_-2844594 4213602_-4440402_-2844594 kind=MOON orbit=0 radius=0.3460685170641377 starId=-1923362853 frame=false - body 4213602_-4440402_-2844594 4213602_-4440402_-2844594 kind=ROGUE_PLANET orbit=0 radius=0.2143152268239313 starId=-1923362853 frame=true - body 4595242_8109101_2801656 4595242_8109101_2801656 kind=MOON orbit=0 radius=0.8302485764437313 starId=-22049925 frame=false - body 4595242_8109101_2801656 4595242_8109101_2801656 kind=ROGUE_PLANET orbit=0 radius=1.614857845425093 starId=-22049925 frame=true - body 6380022_2179216_8117900 6380022_2179216_8117900 kind=MOON orbit=0 radius=0.6122935598736741 starId=-1874711009 frame=false - body 6380022_2179216_8117900 6380022_2179216_8117900 kind=MOON orbit=0 radius=1.7700720105524679 starId=-1874711009 frame=false - body 6380022_2179216_8117900 6380022_2179216_8117900 kind=ROGUE_PLANET orbit=0 radius=2.4803562719196868 starId=-1874711009 frame=true - body 6473135_-2293275_-391540 6473135_-2293275_-391540 kind=ROGUE_PLANET orbit=0 radius=1.2124545908211712 starId=-431200341 frame=true - body 7105709_232763_-3168650 7105699_232763_-3168652 kind=MOON orbit=54 radius=0.21559689240507918 starId=-1067008965 frame=false - body 7105709_232763_-3168650 7105699_232763_-3168652 kind=MOON orbit=54 radius=0.3575168734752142 starId=-1067008965 frame=false - body 7105709_232763_-3168650 7105699_232763_-3168652 kind=PLANET orbit=54 radius=2.1885660950039196 starId=-1067008965 frame=true - body 7105709_232763_-3168650 7105707_232764_-3168669 kind=MOON orbit=103 radius=0.23407286413800515 starId=-1067008965 frame=false - body 7105709_232763_-3168650 7105707_232764_-3168669 kind=PLANET orbit=103 radius=1.7951422258541543 starId=-1067008965 frame=true - body 7105709_232763_-3168650 7105708_232763_-3168647 kind=STAR orbit=16 radius=100.66049123346805 starId=-1067008966 frame=true - body 7105709_232763_-3168650 7105709_232763_-3168650 kind=STAR orbit=0 radius=0.0 starId=-1067008965 frame=true - body 7105709_232763_-3168650 7105776_232759_-3168549 kind=PLANET orbit=646 radius=0.4394546005763268 starId=-1067008965 frame=true - body 7105709_232763_-3168650 7105810_232761_-3168517 kind=ASTEROID_BELT orbit=894 radius=0.0 starId=-1067008965 frame=true - body 7105709_232763_-3168650 7105921_232772_-3169082 kind=ASTEROID_BELT orbit=2576 radius=0.0 starId=-1067008965 frame=true - body 7105709_232763_-3168650 7106003_232766_-3168585 kind=GAS_GIANT orbit=1610 radius=4.978444629240493 starId=-1067008965 frame=true - body 7105709_232763_-3168650 7106003_232766_-3168585 kind=MOON orbit=1610 radius=0.20965217397446498 starId=-1067008965 frame=false - body 7105709_232763_-3168650 7106003_232766_-3168585 kind=MOON orbit=1610 radius=0.34767981578720686 starId=-1067008965 frame=false - body 7105709_232763_-3168650 7106003_232766_-3168585 kind=MOON orbit=1610 radius=0.3970400353089477 starId=-1067008965 frame=false - body 7105709_232763_-3168650 7106003_232766_-3168585 kind=MOON orbit=1610 radius=0.7155332025822625 starId=-1067008965 frame=false - body 7696665_9003901_1665902 7696665_9003901_1665902 kind=ROGUE_PLANET orbit=0 radius=0.5411770116500192 starId=-1655956185 frame=true - body 7791799_600353_1229440 7791799_600353_1229440 kind=MOON orbit=0 radius=1.9632924173437158 starId=-1694756545 frame=false - body 7791799_600353_1229440 7791799_600353_1229440 kind=ROGUE_PLANET orbit=0 radius=2.227881040854203 starId=-1694756545 frame=true - body 7795016_5356799_-122233 7794967_5356796_-122271 kind=PLANET orbit=330 radius=2.0259534223080053 starId=-1185086445 frame=true - body 7795016_5356799_-122233 7795016_5356799_-122229 kind=ASTEROID_BELT orbit=20 radius=0.0 starId=-1185086445 frame=true - body 7795016_5356799_-122233 7795016_5356799_-122233 kind=STAR orbit=0 radius=0.0 starId=-1185086445 frame=true - body 7795016_5356799_-122233 7795017_5356799_-122235 kind=MOON orbit=10 radius=0.36204631187776787 starId=-1185086445 frame=false - body 7795016_5356799_-122233 7795017_5356799_-122235 kind=PLANET orbit=10 radius=1.5197635756613461 starId=-1185086445 frame=true - body 7795016_5356799_-122233 7795022_5356799_-122236 kind=GAS_GIANT orbit=37 radius=4.83698459834699 starId=-1185086445 frame=true - body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.24968217148905664 starId=-1185086445 frame=false - body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.3569127342268691 starId=-1185086445 frame=false - body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.4627425908852818 starId=-1185086445 frame=false - body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.6312409026920827 starId=-1185086445 frame=false - body 7795016_5356799_-122233 7795022_5356799_-122236 kind=MOON orbit=37 radius=0.7410762898997516 starId=-1185086445 frame=false - body 7795016_5356799_-122233 7795092_5356798_-122169 kind=ASTEROID_BELT orbit=528 radius=0.0 starId=-1185086445 frame=true - body 8451398_6964684_8229128 8451398_6964684_8229128 kind=ROGUE_PLANET orbit=0 radius=1.0670964107230607 starId=-1402445041 frame=true - body 9167587_-4662890_1302687 9167587_-4662890_1302687 kind=MOON orbit=0 radius=2.043402615907837 starId=-1312561 frame=false - body 9167587_-4662890_1302687 9167587_-4662890_1302687 kind=ROGUE_PLANET orbit=0 radius=2.2012889250090324 starId=-1312561 frame=true - body 9170317_-4479146_7116367 9164861_-4479305_7117793 kind=ASTEROID_BELT orbit=30166 radius=0.0 starId=-274851001 frame=true - body 9170317_-4479146_7116367 9169176_-4479116_7113031 kind=MOON orbit=18854 radius=0.28236931695299683 starId=-274851001 frame=false - body 9170317_-4479146_7116367 9169176_-4479116_7113031 kind=MOON orbit=18854 radius=0.5826993168629868 starId=-274851001 frame=false - body 9170317_-4479146_7116367 9169176_-4479116_7113031 kind=PLANET orbit=18854 radius=1.0611997784852054 starId=-274851001 frame=true - body 9170317_-4479146_7116367 9169400_-4479146_7116327 kind=STAR orbit=4907 radius=93.78765245497227 starId=-274851002 frame=true - body 9170317_-4479146_7116367 9170250_-4479144_7116312 kind=PLANET orbit=461 radius=0.32472363860513237 starId=-274851001 frame=true - body 9170317_-4479146_7116367 9170317_-4479146_7116367 kind=STAR orbit=0 radius=0.0 starId=-274851001 frame=true - derived -146732_3144538_9058898 -146732_3144538_9058898 type=ice mass=0.022867406045862786 radius=0.365137001300595 gravity=17 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=31987 metallicity=0.9305060662680157 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1990844_4373138_-3056240 -1990792_4373137_-3056198 type=ice mass=2.940584609827984 radius=1.2959853165262225 gravity=175 pressure=1600 tempK=104 oxygen=false locked=false rings=false rotation=9542 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1990844_4373138_-3056240 -1990836_4373138_-3056240 type=superearth mass=16.187528817886992 radius=2.0020815425545506 gravity=400 pressure=1600 tempK=336 oxygen=false locked=true rings=false rotation=15881 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1990844_4373138_-3056240 -1990844_4373138_-3056240 type=lava mass=3.2718283342246335 radius=1.4817161256622684 gravity=149 pressure=42 tempK=1068 oxygen=false locked=true rings=false rotation=84459 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1990844_4373138_-3056240 -1990845_4373138_-3056241 type=barren mass=0.02576612260246133 radius=0.3898083310932754 gravity=17 pressure=0 tempK=335 oxygen=false locked=true rings=false rotation=29642 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1990844_4373138_-3056240 -1990851_4373138_-3056227 type=ice mass=0.04180064524858778 radius=0.3993429730787532 gravity=26 pressure=4 tempK=97 oxygen=false locked=false rings=false rotation=21004 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1990844_4373138_-3056240 -1990857_4373137_-3056263 type=icegiant mass=89.41371358234053 radius=6.336033623503909 gravity=223 pressure=1600 tempK=172 oxygen=false locked=false rings=false rotation=6980 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1990844_4373138_-3056240 -1990868_4373135_-3056136 type=barren mass=0.002204749524119052 radius=0.2030805557303468 gravity=5 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=16289 metallicity=0.7525266188037005 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2174817_-2291967_261255 -2174817_-2291967_261255 type=ice mass=2.313292911863274 radius=1.3479949218897396 gravity=127 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=20830 metallicity=0.6050614095134093 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3680127_6497425_-2974555 -3680127_6497425_-2974555 type=ice mass=8.756134954746999 radius=1.7790054869434075 gravity=277 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=33700 metallicity=0.7822814915510611 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3867498_449288_4118676 -3867498_449288_4118676 type=ice mass=0.00906422581450899 radius=0.2746107364620147 gravity=12 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=15457 metallicity=0.82564227960862 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4373024_7511005_1816766 -4370934_7510969_1815501 type=ice mass=2.33441507382284 radius=1.2856364704578582 gravity=141 pressure=1600 tempK=73 oxygen=false locked=false rings=false rotation=8434 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4373024_7511005_1816766 -4372159_7511003_1818025 type=superearth mass=3.4310841957190874 radius=1.3472896720527072 gravity=189 pressure=1600 tempK=106 oxygen=false locked=false rings=false rotation=11459 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4373024_7511005_1816766 -4372915_7511005_1817033 type=superearth mass=4.257285540746229 radius=1.5094921649735527 gravity=187 pressure=1600 tempK=244 oxygen=false locked=false rings=false rotation=8599 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4373024_7511005_1816766 -4372992_7511006_1816751 type=barren mass=0.17831566243517052 radius=0.6586797028292382 gravity=41 pressure=6 tempK=328 oxygen=false locked=false rings=false rotation=49992 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4373024_7511005_1816766 -4373006_7511005_1816753 type=barren mass=0.1292392825281645 radius=0.6040809505464881 gravity=35 pressure=1 tempK=413 oxygen=false locked=false rings=false rotation=60605 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4373024_7511005_1816766 -4373024_7511005_1816766 type=lava mass=0.06952416485465254 radius=0.4798418385120569 gravity=30 pressure=0 tempK=4539 oxygen=false locked=true rings=false rotation=24606 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4373024_7511005_1816766 -4373066_7511005_1816718 type=gasgiant mass=22.26080765139372 radius=3.461531189557112 gravity=186 pressure=1600 tempK=477 oxygen=false locked=false rings=true rotation=5729 metallicity=0.8790813881853244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4752099_9055058_8335985 -4752099_9055058_8335985 type=ice mass=0.005085643854145261 radius=0.24557082150399945 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=61960 metallicity=0.6851999470392627 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4890357_-2814414_6240914 -4890357_-2814414_6240914 type=ice mass=6.75576181954006 radius=1.6732860210523022 gravity=241 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=74241 metallicity=0.8743399901543765 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -670621_-4121674_-2164509 type=ice mass=8.282097933741516 radius=1.8571244196111838 gravity=240 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=10908 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -670902_-4121657_-2164531 type=icegiant mass=118.13494379261064 radius=7.1517935902824386 gravity=231 pressure=1600 tempK=142 oxygen=false locked=false rings=true rotation=8687 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -670986_-4121661_-2164629 type=barren mass=0.0025692429228558206 radius=0.20105961349957216 gravity=6 pressure=0 tempK=194 oxygen=false locked=false rings=false rotation=72140 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -670989_-4121660_-2164613 type=greenhouse mass=24.65940033203534 radius=2.341480090625241 gravity=400 pressure=1600 tempK=915 oxygen=false locked=true rings=false rotation=60344 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -670991_-4121660_-2164613 type=lava mass=0.005455754543576298 radius=0.23410640865996032 gravity=10 pressure=0 tempK=1858 oxygen=false locked=true rings=false rotation=93940 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -670996_-4121660_-2164616 type=barren mass=0.009047298081132884 radius=0.27867397404199995 gravity=12 pressure=0 tempK=331 oxygen=false locked=true rings=false rotation=48589 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -671002_-4121659_-2164570 type=superearth mass=7.9096413631715885 radius=1.8533917606053845 gravity=230 pressure=1600 tempK=254 oxygen=false locked=false rings=false rotation=8355 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -671003_-4121660_-2164619 type=barren mass=0.13073198117259513 radius=0.5699188917970125 gravity=40 pressure=2 tempK=219 oxygen=false locked=false rings=false rotation=10344 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -671008_-4121660_-2164588 type=gasgiant mass=33.75495820252818 radius=4.148350959171349 gravity=196 pressure=1600 tempK=283 oxygen=false locked=false rings=true rotation=10260 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -670991_-4121660_-2164613 -671165_-4121668_-2164448 type=ice mass=4.397623655143227 radius=1.518213839049826 gravity=191 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=78861 metallicity=0.9514596527295555 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1191593_-4530197_6633767 type=ice mass=0.029366891031306075 radius=0.3710815068256704 gravity=21 pressure=8 tempK=38 oxygen=false locked=false rings=false rotation=58853 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1194117_-4530031_6629817 type=gasgiant mass=19.392650674542935 radius=3.260041612564983 gravity=182 pressure=1600 tempK=346 oxygen=false locked=false rings=false rotation=10334 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1194162_-4530030_6629610 type=barren mass=0.22033439440422145 radius=0.6761236797406471 gravity=48 pressure=9 tempK=238 oxygen=false locked=false rings=false rotation=9477 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1194189_-4530017_6629650 type=superearth mass=11.798299735529016 radius=1.910283521314024 gravity=323 pressure=1600 tempK=520 oxygen=false locked=false rings=false rotation=44876 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1194309_-4530025_6629559 type=desert mass=0.10652823903410986 radius=0.5175670411566737 gravity=40 pressure=0 tempK=511 oxygen=false locked=false rings=false rotation=6003 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1194313_-4530027_6629512 type=superearth mass=13.406205618009157 radius=1.986963906941915 gravity=340 pressure=1600 tempK=898 oxygen=false locked=false rings=false rotation=10515 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1194346_-4530025_6629562 type=unclassified mass=0.7358493099176111 radius=0.9648973832295691 gravity=79 pressure=0 tempK=7177 oxygen=false locked=true rings=false rotation=47233 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1194351_-4530025_6629565 type=lava mass=5.989648456196548 radius=1.6243169204923151 gravity=227 pressure=107 tempK=1570 oxygen=false locked=true rings=false rotation=36181 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1194475_-4530022_6629580 type=greenhouse mass=3.521637322683175 radius=1.4999718538392695 gravity=157 pressure=1600 tempK=473 oxygen=false locked=false rings=false rotation=40417 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1194906_-4530040_6629503 type=gasgiant mass=50.51342206119997 radius=4.943021985795619 gravity=207 pressure=1600 tempK=270 oxygen=false locked=false rings=true rotation=5691 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1195455_-4529982_6629794 type=ice mass=1.540739662341268 radius=1.081029895238314 gravity=132 pressure=1600 tempK=180 oxygen=false locked=false rings=true rotation=29964 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1195499_-4530033_6628603 type=barren mass=0.07638558844916687 radius=0.4745120039726671 gravity=34 pressure=21 tempK=84 oxygen=false locked=false rings=false rotation=83853 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1195668_-4530126_6632475 type=gasgiant mass=112.56786087925825 radius=7.003259256804836 gravity=230 pressure=1600 tempK=113 oxygen=false locked=false rings=false rotation=6319 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1194346_-4530025_6629562 1196104_-4530132_6637414 type=ice mass=21.669836792937 radius=2.1679276523685638 gravity=400 pressure=1600 tempK=67 oxygen=false locked=false rings=false rotation=21439 metallicity=0.3867705861157173 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1756923_-1971171_998832 1756304_-1971195_999471 type=ice mass=0.0270649648764436 radius=0.36539458876974434 gravity=20 pressure=4 tempK=74 oxygen=false locked=false rings=false rotation=21192 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1756923_-1971171_998832 1756802_-1971224_997414 type=ice mass=0.9932954039372163 radius=0.9585396474024162 gravity=108 pressure=1600 tempK=132 oxygen=false locked=false rings=false rotation=12846 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1756923_-1971171_998832 1756895_-1971172_998845 type=barren mass=0.002677086174508008 radius=0.2058604344637433 gravity=6 pressure=0 tempK=289 oxygen=false locked=false rings=false rotation=59451 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1756923_-1971171_998832 1756923_-1971171_998832 type=lava mass=10.54939105687714 radius=1.9434421937694413 gravity=279 pressure=51 tempK=2836 oxygen=false locked=true rings=false rotation=70186 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1756923_-1971171_998832 1756927_-1971170_998853 type=desert mass=0.1919300415202249 radius=0.6445989983146653 gravity=46 pressure=2 tempK=296 oxygen=false locked=false rings=false rotation=7914 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1756923_-1971171_998832 1756928_-1971171_998828 type=barren mass=0.19517868947521144 radius=0.6410672902404898 gravity=47 pressure=1 tempK=487 oxygen=false locked=true rings=false rotation=13411 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1756923_-1971171_998832 1757015_-1971171_998790 type=desert mass=0.4602241031797454 radius=0.8145284093856098 gravity=69 pressure=24 tempK=216 oxygen=false locked=false rings=false rotation=9149 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1756923_-1971171_998832 1757193_-1971162_999102 type=exotic mass=2.749365528960417 radius=1.3665356678264369 gravity=147 pressure=1600 tempK=290 oxygen=false locked=false rings=false rotation=7602 metallicity=1.477028525733271 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1836710_7713193_-3440196 1836710_7713193_-3440196 type=ice mass=0.967637822427201 radius=0.9935421843295233 gravity=98 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=57631 metallicity=1.5477966882660805 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2469282_4416743_-723947 2469276_4416743_-723937 type=gasgiant mass=55.70351201841058 radius=5.157749853921637 gravity=209 pressure=1600 tempK=216 oxygen=false locked=false rings=true rotation=6897 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2469282_4416743_-723947 2469282_4416743_-723942 type=ice mass=0.4300884439870668 radius=0.8567827527241039 gravity=59 pressure=94 tempK=159 oxygen=false locked=true rings=false rotation=38289 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2469282_4416743_-723947 2469282_4416743_-723947 type=barren mass=0.4517766972918707 radius=0.863002096992908 gravity=61 pressure=1 tempK=894 oxygen=false locked=true rings=false rotation=39145 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2469282_4416743_-723947 2469283_4416743_-723947 type=superearth mass=5.538914624136937 radius=1.6637654823213972 gravity=200 pressure=1134 tempK=658 oxygen=false locked=true rings=false rotation=12253 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2469282_4416743_-723947 2469303_4416743_-723951 type=gasgiant mass=164.1820025391923 radius=8.252127224356407 gravity=241 pressure=1600 tempK=161 oxygen=false locked=false rings=true rotation=9187 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2469282_4416743_-723947 2469310_4416743_-723868 type=ice mass=0.39810198730358975 radius=0.7379293830014126 gravity=73 pressure=491 tempK=58 oxygen=false locked=false rings=false rotation=13184 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2469282_4416743_-723947 2469310_4416744_-723991 type=barren mass=0.022462386186608514 radius=0.3860558158888115 gravity=15 pressure=1 tempK=53 oxygen=false locked=false rings=false rotation=7895 metallicity=0.3815590086572219 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2948486_1502429_3559523 2948486_1502429_3559523 type=ice mass=21.212812738793687 radius=2.4054665038276375 gravity=367 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=37771 metallicity=1.4470347033190816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3510787_4170368_5776814 3510787_4170368_5776814 type=ice mass=1.5534085098665662 radius=1.1577006008040243 gravity=116 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=27402 metallicity=1.3243201471707986 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3709337_6524042_9099103 3709202_6524034_9098955 type=barren mass=0.002930641312528724 radius=0.20200695258004708 gravity=7 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=15666 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3709337_6524042_9099103 3709286_6524045_9099143 type=ice mass=0.003314115116235893 radius=0.20215208272759683 gravity=8 pressure=0 tempK=68 oxygen=false locked=false rings=false rotation=70775 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3709337_6524042_9099103 3709337_6524042_9099103 type=barren mass=0.0023816022881015756 radius=0.2008675570768562 gravity=6 pressure=0 tempK=866 oxygen=false locked=true rings=false rotation=9655 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3709337_6524042_9099103 3709337_6524042_9099104 type=barren mass=0.002568901725669564 radius=0.2024093892049266 gravity=6 pressure=0 tempK=321 oxygen=false locked=true rings=false rotation=7242 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3709337_6524042_9099103 3709343_6524042_9099110 type=greenhouse mass=26.03102594223896 radius=2.309122467818324 gravity=400 pressure=1600 tempK=304 oxygen=false locked=false rings=false rotation=10673 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3709337_6524042_9099103 3709393_6524040_9098991 type=ice mass=0.01208302049187343 radius=0.317794798051077 gravity=12 pressure=3 tempK=49 oxygen=false locked=false rings=false rotation=16818 metallicity=0.8637879897579199 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4213602_-4440402_-2844594 4213602_-4440402_-2844594 type=barren mass=0.003008765886724551 radius=0.2143152268239313 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=69971 metallicity=0.7299135708064022 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4595242_8109101_2801656 4595242_8109101_2801656 type=ice mass=7.252268911685418 radius=1.614857845425093 gravity=278 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=6343 metallicity=0.8659153883644486 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6380022_2179216_8117900 6380022_2179216_8117900 type=superearth mass=23.61670682531038 radius=2.4803562719196868 gravity=384 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=83230 metallicity=0.5776707870302991 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6473135_-2293275_-391540 6473135_-2293275_-391540 type=ice mass=1.9964756602479061 radius=1.2124545908211712 gravity=136 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=9299 metallicity=1.104939030674866 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7105709_232763_-3168650 7105699_232763_-3168652 type=greenhouse mass=22.49121722974076 radius=2.1885660950039196 gravity=400 pressure=1600 tempK=603 oxygen=false locked=false rings=false rotation=17608 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7105709_232763_-3168650 7105707_232764_-3168669 type=superearth mass=7.147461200654439 radius=1.7951422258541543 gravity=222 pressure=1600 tempK=571 oxygen=false locked=false rings=false rotation=9877 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7105709_232763_-3168650 7105708_232763_-3168647 type=desert mass=2.181410071978156 radius=1.307253044809006 gravity=128 pressure=38 tempK=569 oxygen=false locked=true rings=false rotation=24769 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7105709_232763_-3168650 7105709_232763_-3168650 type=lava mass=0.004534199803455963 radius=0.22708391874861367 gravity=9 pressure=0 tempK=1838 oxygen=false locked=true rings=false rotation=21113 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7105709_232763_-3168650 7105776_232759_-3168549 type=barren mass=0.05003200502122312 radius=0.4394546005763268 gravity=26 pressure=4 tempK=107 oxygen=false locked=false rings=false rotation=56490 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7105709_232763_-3168650 7105810_232761_-3168517 type=barren mass=0.01515770663921861 radius=0.33398669745811155 gravity=14 pressure=1 tempK=91 oxygen=false locked=false rings=false rotation=74044 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7105709_232763_-3168650 7105921_232772_-3169082 type=icegiant mass=107.7729463921819 radius=6.871962609713998 gravity=228 pressure=1600 tempK=105 oxygen=false locked=false rings=true rotation=11956 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7105709_232763_-3168650 7106003_232766_-3168585 type=gasgiant mass=51.34987738801249 radius=4.978444629240493 gravity=207 pressure=1600 tempK=133 oxygen=false locked=false rings=true rotation=8750 metallicity=1.5249939165527286 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7696665_9003901_1665902 7696665_9003901_1665902 type=barren mass=0.09962695429765411 radius=0.5411770116500192 gravity=34 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=9596 metallicity=1.4134633815807982 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7791799_600353_1229440 7791799_600353_1229440 type=superearth mass=19.254385948976793 radius=2.227881040854203 gravity=388 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=19160 metallicity=1.0481893678631202 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7795016_5356799_-122233 7794967_5356796_-122271 type=ice mass=14.578610762625457 radius=2.0259534223080053 gravity=355 pressure=1600 tempK=93 oxygen=false locked=false rings=false rotation=13699 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7795016_5356799_-122233 7795016_5356799_-122229 type=gasgiant mass=219.14501235596202 radius=9.356001465066456 gravity=250 pressure=1600 tempK=403 oxygen=false locked=false rings=true rotation=7519 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7795016_5356799_-122233 7795016_5356799_-122233 type=lava mass=6.6310319663732535 radius=1.6508264151239065 gravity=243 pressure=317 tempK=1394 oxygen=false locked=true rings=false rotation=20348 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7795016_5356799_-122233 7795017_5356799_-122235 type=superearth mass=3.8406646125641988 radius=1.5197635756613461 gravity=166 pressure=890 tempK=535 oxygen=false locked=true rings=false rotation=7771 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7795016_5356799_-122233 7795022_5356799_-122236 type=gasgiant mass=48.05579480668342 radius=4.83698459834699 gravity=205 pressure=1600 tempK=296 oxygen=false locked=false rings=true rotation=12224 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7795016_5356799_-122233 7795092_5356798_-122169 type=barren mass=0.0033540793281609635 radius=0.2039556489264513 gravity=8 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=18724 metallicity=0.8395807111324967 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8451398_6964684_8229128 8451398_6964684_8229128 type=ice mass=1.2382410107593795 radius=1.0670964107230607 gravity=109 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=14466 metallicity=0.39658577524042765 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9167587_-4662890_1302687 9167587_-4662890_1302687 type=ice mass=21.63652698948589 radius=2.2012889250090324 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=31418 metallicity=0.3755972598382573 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9170317_-4479146_7116367 9164861_-4479305_7117793 type=ice mass=0.21076566380930786 radius=0.6213603213580874 gravity=55 pressure=209 tempK=51 oxygen=false locked=false rings=false rotation=24568 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9170317_-4479146_7116367 9169176_-4479116_7113031 type=ice mass=1.5325568551099056 radius=1.0611997784852054 gravity=136 pressure=1600 tempK=108 oxygen=false locked=false rings=false rotation=54585 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9170317_-4479146_7116367 9169400_-4479146_7116327 type=ice mass=0.024929585238438568 radius=0.35868099901754724 gravity=19 pressure=3 tempK=94 oxygen=false locked=false rings=false rotation=79964 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9170317_-4479146_7116367 9170250_-4479144_7116312 type=barren mass=0.014119376228983096 radius=0.32472363860513237 gravity=13 pressure=0 tempK=376 oxygen=false locked=false rings=false rotation=25222 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9170317_-4479146_7116367 9170317_-4479146_7116367 type=unclassified mass=20.97066498348173 radius=2.205923574177095 gravity=400 pressure=9 tempK=7626 oxygen=false locked=true rings=false rotation=8222 metallicity=0.9345203272091593 terrain=TerrainOption[NATIVE genType=0 w=1] - system -146732_3144538_9058898 id=-478929905 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless - system -1990844_4373138_-3056240 id=-1524375109 kind=STAR name=PGS--5002361.0.-5002361 starTemp=40 starSize=0.9608185887336731 - system -2174817_-2291967_261255 id=-1472462165 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless - system -3680127_6497425_-2974555 id=-1983124585 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless - system -3867498_449288_4118676 id=-1943963613 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless - system -4373024_7511005_1816766 id=-110594437 kind=STAR name=PGS--5002361.5002361.0 starTemp=150 starSize=1.2341642379760742 - system -4752099_9055058_8335985 id=-392839469 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless - system -4890357_-2814414_6240914 id=-759681393 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless - system -670991_-4121660_-2164613 id=-262321917 kind=STAR name=PGS--5002361.-5002361.-5002361 starTemp=70 starSize=0.9494102001190186 - system 1194346_-4530025_6629562 id=-962147133 kind=STAR name=PGS-0.-5002361.5002361 starTemp=220 starSize=1.6262410879135132 - system 1756923_-1971171_998832 id=-1811991613 kind=STAR name=PGS-0.-5002361.0 starTemp=100 starSize=1.0837022066116333 - system 1836710_7713193_-3440196 id=-1441927509 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless - system 2469282_4416743_-723947 id=-776084721 kind=STAR name=PGS-0.0.-5002361 starTemp=40 starSize=0.6808884739875793 - system 2948486_1502429_3559523 id=-875095761 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 3510787_4170368_5776814 id=-205539377 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless - system 3709337_6524042_9099103 id=-1795014233 kind=STAR name=PGS-0.5002361.5002361 starTemp=40 starSize=0.6385629773139954 - system 4213602_-4440402_-2844594 id=-1923362853 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless - system 4595242_8109101_2801656 id=-22049925 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless - system 6380022_2179216_8117900 id=-1874711009 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless - system 6473135_-2293275_-391540 id=-431200341 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless - system 7105709_232763_-3168650 id=-1067008965 kind=STAR name=PGS-5002361.0.-5002361 starTemp=70 starSize=0.9220526814460754 - system 7696665_9003901_1665902 id=-1655956185 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless - system 7791799_600353_1229440 id=-1694756545 kind=ROGUE_PLANET name=PGR-5002361.0.0 starless - system 7795016_5356799_-122233 id=-1185086445 kind=STAR name=PGS-5002361.5002361.-5002361 starTemp=40 starSize=0.7262133359909058 - system 8451398_6964684_8229128 id=-1402445041 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless - system 9167587_-4662890_1302687 id=-1312561 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless - system 9170317_-4479146_7116367 id=-274851001 kind=STAR name=PGS-5002361.-5002361.5002361 starTemp=220 starSize=1.8361765146255493 + body -1230314_1893274_-2189019 -1230314_1893274_-2189019 kind=MOON orbit=0 radius=1.1108639180445716 starId=-1485753477 frame=false at=-46711,0,-41375 + body -1230314_1893274_-2189019 -1230314_1893274_-2189019 kind=ROGUE_PLANET orbit=0 radius=0.3271755891768876 starId=-1485753477 frame=true at=0,0,0 + body -1735123_-1297562_-3230849 -1735123_-1297562_-3230849 kind=ROGUE_PLANET orbit=0 radius=0.9122063191004484 starId=-457888649 frame=true at=0,0,0 + body -1839127_-930469_3024425 -1839127_-930469_3024425 kind=ROGUE_PLANET orbit=0 radius=1.2352684367448383 starId=-1123773025 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2967914_5695077_-2605367 kind=ASTEROID_BELT orbit=974 radius=0.0 starId=-423794625 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968046_5695074_-2605415 kind=PLANET orbit=255 radius=1.0703624393599125 starId=-423794625 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968054_5695071_-2605430 kind=PLANET orbit=162 radius=1.1112601957037147 starId=-423794625 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968066_5695072_-2605456 kind=ASTEROID_BELT orbit=44 radius=0.0 starId=-423794625 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968066_5695072_-2605466 kind=GAS_GIANT orbit=80 radius=5.032996057500449 starId=-423794625 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968066_5695072_-2605466 kind=MOON orbit=80 radius=0.3381283400377148 starId=-423794625 frame=false at=-429729,0,-381141 + body -2968074_5695072_-2605453 -2968066_5695072_-2605466 kind=MOON orbit=80 radius=0.3442873389359359 starId=-423794625 frame=false at=68040,0,-1111119 + body -2968074_5695072_-2605453 -2968073_5695072_-2605452 kind=STAR orbit=8 radius=79.49606685519218 starId=-423794626 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968074_5695072_-2605453 kind=STAR orbit=0 radius=0.0 starId=-423794625 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968075_5695072_-2605459 kind=PLANET orbit=31 radius=0.5085798172977589 starId=-423794625 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968080_5695072_-2605444 kind=PLANET orbit=57 radius=1.2180207924065842 starId=-423794625 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968141_5695075_-2605545 kind=GAS_GIANT orbit=609 radius=6.559618412306246 starId=-423794625 frame=true at=0,0,0 + body -2968074_5695072_-2605453 -2968198_5695072_-2605067 kind=STAR orbit=2168 radius=79.49606685519218 starId=-423794627 frame=true at=0,0,0 + body -3037636_-286450_6061438 -3037636_-286450_6061438 kind=MOON orbit=0 radius=0.20176280372394176 starId=-772485517 frame=false at=171483,0,-205174 + body -3037636_-286450_6061438 -3037636_-286450_6061438 kind=MOON orbit=0 radius=1.9715799845010111 starId=-772485517 frame=false at=-122688,0,-224913 + body -3037636_-286450_6061438 -3037636_-286450_6061438 kind=ROGUE_PLANET orbit=0 radius=1.1321185551403936 starId=-772485517 frame=true at=0,0,0 + body -3258554_1675500_3803298 -3258547_1675497_3803188 kind=ASTEROID_BELT orbit=588 radius=0.0 starId=-472927549 frame=true at=0,0,0 + body -3258554_1675500_3803298 -3258547_1675499_3803284 kind=GAS_GIANT orbit=84 radius=5.6716793111039 starId=-472927549 frame=true at=0,0,0 + body -3258554_1675500_3803298 -3258554_1675500_3803298 kind=STAR orbit=0 radius=0.0 starId=-472927549 frame=true at=0,0,0 + body -3258554_1675500_3803298 -3258555_1675500_3803299 kind=PLANET orbit=8 radius=1.0933516689678018 starId=-472927549 frame=true at=0,0,0 + body -3258554_1675500_3803298 -3258562_1675500_3803301 kind=ASTEROID_BELT orbit=46 radius=0.0 starId=-472927549 frame=true at=0,0,0 + body -3258554_1675500_3803298 -3258563_1675497_3803230 kind=MOON orbit=368 radius=0.527188535681306 starId=-472927549 frame=false at=45455,0,41574 + body -3258554_1675500_3803298 -3258563_1675497_3803230 kind=MOON orbit=368 radius=0.7108651577260094 starId=-472927549 frame=false at=-18923,0,3250 + body -3258554_1675500_3803298 -3258563_1675497_3803230 kind=PLANET orbit=368 radius=0.22596366099897897 starId=-472927549 frame=true at=0,0,0 + body -545630_6755069_2561467 -545471_6755071_2561465 kind=GAS_GIANT orbit=853 radius=10.105985698063467 starId=-1674485141 frame=true at=0,0,0 + body -545630_6755069_2561467 -545471_6755071_2561465 kind=MOON orbit=853 radius=0.27739774330825634 starId=-1674485141 frame=false at=-209250,0,1087654 + body -545630_6755069_2561467 -545471_6755071_2561465 kind=MOON orbit=853 radius=0.4474341471900287 starId=-1674485141 frame=false at=-417841,0,-2284502 + body -545630_6755069_2561467 -545611_6755062_2561721 kind=ASTEROID_BELT orbit=1364 radius=0.0 starId=-1674485141 frame=true at=0,0,0 + body -545630_6755069_2561467 -545624_6755069_2561468 kind=STAR orbit=34 radius=85.67155276358127 starId=-1674485142 frame=true at=0,0,0 + body -545630_6755069_2561467 -545630_6755069_2561467 kind=STAR orbit=0 radius=0.0 starId=-1674485141 frame=true at=0,0,0 + body -545630_6755069_2561467 -545634_6755067_2561379 kind=GAS_GIANT orbit=473 radius=4.353696915708663 starId=-1674485141 frame=true at=0,0,0 + body -545630_6755069_2561467 -545634_6755067_2561379 kind=MOON orbit=473 radius=0.23383540089451044 starId=-1674485141 frame=false at=-859794,0,-774510 + body -545630_6755069_2561467 -545646_6755070_2561489 kind=MOON orbit=143 radius=0.39785081213673035 starId=-1674485141 frame=false at=25172,0,-34862 + body -545630_6755069_2561467 -545646_6755070_2561489 kind=PLANET orbit=143 radius=0.2807380026340163 starId=-1674485141 frame=true at=0,0,0 + body -545630_6755069_2561467 -545654_6755068_2561424 kind=ASTEROID_BELT orbit=262 radius=0.0 starId=-1674485141 frame=true at=0,0,0 + body -755250_5247165_5302015 -755250_5247165_5302015 kind=ROGUE_PLANET orbit=0 radius=1.5423697863264323 starId=-157245369 frame=true at=0,0,0 + body -758984_1485681_3107299 -758984_1485681_3107299 kind=MOON orbit=0 radius=0.6401362407909844 starId=-726144233 frame=false at=-121535,0,421425 + body -758984_1485681_3107299 -758984_1485681_3107299 kind=MOON orbit=0 radius=1.7493057530044385 starId=-726144233 frame=false at=108838,0,-324430 + body -758984_1485681_3107299 -758984_1485681_3107299 kind=ROGUE_PLANET orbit=0 radius=2.484840931637559 starId=-726144233 frame=true at=0,0,0 + body 1159501_438377_2970042 1159501_438377_2970042 kind=ROGUE_PLANET orbit=0 radius=0.2438710079048173 starId=-875095761 frame=true at=0,0,0 + body 1502575_5241619_578848 1502575_5241619_578848 kind=ROGUE_PLANET orbit=0 radius=2.2738202904677567 starId=-440778097 frame=true at=0,0,0 + body 1915131_1673389_-1943074 1915131_1673389_-1943074 kind=MOON orbit=0 radius=0.21608366869246257 starId=-268045669 frame=false at=39280,0,26530 + body 1915131_1673389_-1943074 1915131_1673389_-1943074 kind=ROGUE_PLANET orbit=0 radius=0.5320514501890937 starId=-268045669 frame=true at=0,0,0 + body 2639269_-1847617_-2599924 2639190_-1847611_-2600049 kind=MOON orbit=791 radius=0.5916428926581476 starId=-1586685861 frame=false at=61277,0,-9439 + body 2639269_-1847617_-2599924 2639190_-1847611_-2600049 kind=PLANET orbit=791 radius=0.3444434132626236 starId=-1586685861 frame=true at=0,0,0 + body 2639269_-1847617_-2599924 2639267_-1847617_-2599925 kind=MOON orbit=11 radius=0.2025456744373965 starId=-1586685861 frame=false at=67016,0,-45851 + body 2639269_-1847617_-2599924 2639267_-1847617_-2599925 kind=MOON orbit=11 radius=0.422375278424855 starId=-1586685861 frame=false at=-18140,0,-51287 + body 2639269_-1847617_-2599924 2639267_-1847617_-2599925 kind=PLANET orbit=11 radius=0.29987989003936566 starId=-1586685861 frame=true at=0,0,0 + body 2639269_-1847617_-2599924 2639269_-1847617_-2599924 kind=STAR orbit=0 radius=0.0 starId=-1586685861 frame=true at=0,0,0 + body 2639269_-1847617_-2599924 2639272_-1847617_-2599928 kind=PLANET orbit=28 radius=0.369897875724714 starId=-1586685861 frame=true at=0,0,0 + body 2639269_-1847617_-2599924 2639274_-1847617_-2599896 kind=STAR orbit=152 radius=84.70957162857056 starId=-1586685864 frame=true at=0,0,0 + body 2639269_-1847617_-2599924 2639288_-1847617_-2599929 kind=STAR orbit=105 radius=84.70957162857056 starId=-1586685863 frame=true at=0,0,0 + body 2639269_-1847617_-2599924 2639505_-1847606_-2599928 kind=ASTEROID_BELT orbit=1265 radius=0.0 starId=-1586685861 frame=true at=0,0,0 + body 2639269_-1847617_-2599924 2641577_-1847617_-2605840 kind=STAR orbit=33958 radius=79.98081523776054 starId=-1586685862 frame=true at=0,0,0 + body 2646744_5580083_-2997664 2646704_5580086_-2997730 kind=ASTEROID_BELT orbit=414 radius=0.0 starId=-1182950741 frame=true at=0,0,0 + body 2646744_5580083_-2997664 2646744_5580083_-2997664 kind=STAR orbit=0 radius=0.0 starId=-1182950741 frame=true at=0,0,0 + body 2646744_5580083_-2997664 2646746_5580083_-2997663 kind=MOON orbit=13 radius=0.47444771458559326 starId=-1182950741 frame=false at=331593,0,-173844 + body 2646744_5580083_-2997664 2646746_5580083_-2997663 kind=PLANET orbit=13 radius=2.3360233067337304 starId=-1182950741 frame=true at=0,0,0 + body 2646744_5580083_-2997664 2646748_5580083_-2997658 kind=MOON orbit=39 radius=0.6828298515923701 starId=-1182950741 frame=false at=118099,0,49363 + body 2646744_5580083_-2997664 2646748_5580083_-2997658 kind=PLANET orbit=39 radius=1.7389342248982458 starId=-1182950741 frame=true at=0,0,0 + body 2646744_5580083_-2997664 2646762_5580082_-2997709 kind=MOON orbit=259 radius=0.5452012592227564 starId=-1182950741 frame=false at=-40362,0,-1763 + body 2646744_5580083_-2997664 2646762_5580082_-2997709 kind=PLANET orbit=259 radius=0.2127699482289579 starId=-1182950741 frame=true at=0,0,0 + body 2868006_4842033_4291324 2868006_4842033_4291324 kind=ROGUE_PLANET orbit=0 radius=1.7074633543397322 starId=-1484690657 frame=true at=0,0,0 + body 3210136_-1258479_1657446 3210128_-1258480_1657458 kind=MOON orbit=77 radius=0.2148191821741564 starId=-143401165 frame=false at=-53418,0,-141877 + body 3210136_-1258479_1657446 3210128_-1258480_1657458 kind=MOON orbit=77 radius=0.503761806492902 starId=-143401165 frame=false at=-65353,0,-118355 + body 3210136_-1258479_1657446 3210128_-1258480_1657458 kind=PLANET orbit=77 radius=1.0175020841222762 starId=-143401165 frame=true at=0,0,0 + body 3210136_-1258479_1657446 3210136_-1258479_1657446 kind=STAR orbit=0 radius=0.0 starId=-143401165 frame=true at=0,0,0 + body 3210136_-1258479_1657446 3210136_-1258479_1657447 kind=MOON orbit=7 radius=0.33538944125555403 starId=-143401165 frame=false at=149822,0,38139 + body 3210136_-1258479_1657446 3210136_-1258479_1657447 kind=PLANET orbit=7 radius=0.5614017489351886 starId=-143401165 frame=true at=0,0,0 + body 3210136_-1258479_1657446 3210142_-1258479_1657449 kind=PLANET orbit=35 radius=0.9817404084193058 starId=-143401165 frame=true at=0,0,0 + body 3210136_-1258479_1657446 3210151_-1258481_1657352 kind=ASTEROID_BELT orbit=507 radius=0.0 starId=-143401165 frame=true at=0,0,0 + body 3210136_-1258479_1657446 3210152_-1258477_1657503 kind=PLANET orbit=317 radius=1.924905001225997 starId=-143401165 frame=true at=0,0,0 + body 3405269_-1627426_6553174 3405269_-1627426_6553174 kind=ROGUE_PLANET orbit=0 radius=1.0902957609031467 starId=-1023282445 frame=true at=0,0,0 + body 3888408_1951506_402528 3866811_1951506_398707 kind=STAR orbit=117288 radius=94.20303580105305 starId=-1169505182 frame=true at=0,0,0 + body 3888408_1951506_402528 3888302_1951507_402590 kind=ASTEROID_BELT orbit=657 radius=0.0 starId=-1169505181 frame=true at=0,0,0 + body 3888408_1951506_402528 3888398_1951506_402530 kind=GAS_GIANT orbit=54 radius=8.213303587243715 starId=-1169505181 frame=true at=0,0,0 + body 3888408_1951506_402528 3888405_1951506_402529 kind=GAS_GIANT orbit=19 radius=7.863525818272932 starId=-1169505181 frame=true at=0,0,0 + body 3888408_1951506_402528 3888405_1951506_402529 kind=MOON orbit=19 radius=0.2028788302456146 starId=-1169505181 frame=false at=-389688,0,1835898 + body 3888408_1951506_402528 3888405_1951506_402529 kind=MOON orbit=19 radius=0.20609414861856426 starId=-1169505181 frame=false at=-1093080,0,-1854882 + body 3888408_1951506_402528 3888405_1951506_402529 kind=MOON orbit=19 radius=0.23354633994491927 starId=-1169505181 frame=false at=-1133377,0,-742252 + body 3888408_1951506_402528 3888405_1951506_402529 kind=MOON orbit=19 radius=0.5732224397545107 starId=-1169505181 frame=false at=650319,0,454150 + body 3888408_1951506_402528 3888407_1951506_402529 kind=ASTEROID_BELT orbit=10 radius=0.0 starId=-1169505181 frame=true at=0,0,0 + body 3888408_1951506_402528 3888408_1951506_402528 kind=STAR orbit=0 radius=0.0 starId=-1169505181 frame=true at=0,0,0 + body 3888408_1951506_402528 3888408_1951506_402530 kind=PLANET orbit=10 radius=0.4653093968013708 starId=-1169505181 frame=true at=0,0,0 + body 3888408_1951506_402528 3888409_1951506_402514 kind=GAS_GIANT orbit=74 radius=6.4434807244767 starId=-1169505181 frame=true at=0,0,0 + body 3888408_1951506_402528 3888409_1951506_402514 kind=MOON orbit=74 radius=0.27307165317760784 starId=-1169505181 frame=false at=-747621,0,1064981 + body 3888408_1951506_402528 3888409_1951506_402514 kind=MOON orbit=74 radius=0.3788747613111544 starId=-1169505181 frame=false at=645061,0,484252 + body 3888408_1951506_402528 3888409_1951506_402514 kind=MOON orbit=74 radius=0.4567167848973427 starId=-1169505181 frame=false at=433362,0,1066517 + body 3888408_1951506_402528 3888409_1951506_402514 kind=MOON orbit=74 radius=0.5702453561500446 starId=-1169505181 frame=false at=-430023,0,1012245 + body 3888408_1951506_402528 3888419_1951505_402484 kind=MOON orbit=240 radius=0.5460815191564856 starId=-1169505181 frame=false at=28983,0,-249522 + body 3888408_1951506_402528 3888419_1951505_402484 kind=MOON orbit=240 radius=0.7054016448066485 starId=-1169505181 frame=false at=-110458,0,-102956 + body 3888408_1951506_402528 3888419_1951505_402484 kind=PLANET orbit=240 radius=2.0003231974248514 starId=-1169505181 frame=true at=0,0,0 + body 3888408_1951506_402528 3888426_1951506_402453 kind=MOON orbit=411 radius=0.2080090024469129 starId=-1169505181 frame=false at=-515123,0,-354646 + body 3888408_1951506_402528 3888426_1951506_402453 kind=PLANET orbit=411 radius=2.481279125837847 starId=-1169505181 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4433045_-1299745_6671676 kind=STAR orbit=9636 radius=93.87084494948387 starId=-779017546 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434199_-1299751_6672916 kind=ASTEROID_BELT orbit=793 radius=0.0 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434341_-1299745_6672928 kind=PLANET orbit=48 radius=1.029447635674642 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434347_-1299745_6672921 kind=STAR orbit=0 radius=0.0 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434347_-1299745_6672926 kind=MOON orbit=29 radius=0.6746619882477115 starId=-779017545 frame=false at=1017,0,-35585 + body 4434347_-1299745_6672921 4434347_-1299745_6672926 kind=PLANET orbit=29 radius=0.5488513114065555 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434348_-1299745_6672921 kind=PLANET orbit=7 radius=1.1564058055441164 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434349_-1299745_6672922 kind=MOON orbit=13 radius=0.3128534811459255 starId=-779017545 frame=false at=49238,0,5979 + body 4434347_-1299745_6672921 4434349_-1299745_6672922 kind=MOON orbit=13 radius=0.4126258227463259 starId=-779017545 frame=false at=-134255,0,65999 + body 4434347_-1299745_6672921 4434349_-1299745_6672922 kind=PLANET orbit=13 radius=0.5263349713162284 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434355_-1299745_6672923 kind=ASTEROID_BELT orbit=42 radius=0.0 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=GAS_GIANT orbit=133 radius=7.415370376903965 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=MOON orbit=133 radius=0.21437447594308398 starId=-779017545 frame=false at=1616783,0,223806 + body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=MOON orbit=133 radius=0.40440708215634125 starId=-779017545 frame=false at=701850,0,-794359 + body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=MOON orbit=133 radius=0.6308604975479772 starId=-779017545 frame=false at=31306,0,1042130 + body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=MOON orbit=133 radius=0.6577279267763994 starId=-779017545 frame=false at=-606066,0,943991 + body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=GAS_GIANT orbit=77 radius=10.678467980226468 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=MOON orbit=77 radius=0.2661410111181117 starId=-779017545 frame=false at=-2886152,0,-482466 + body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=MOON orbit=77 radius=0.36269510031743357 starId=-779017545 frame=false at=-1794557,0,-1940364 + body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=MOON orbit=77 radius=0.36605921118550216 starId=-779017545 frame=false at=141975,0,-2155529 + body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=MOON orbit=77 radius=0.3744273970489842 starId=-779017545 frame=false at=2174829,0,1051764 + body 4434347_-1299745_6672921 4434396_-1299748_6672946 kind=GAS_GIANT orbit=295 radius=8.735151803026602 starId=-779017545 frame=true at=0,0,0 + body 4434347_-1299745_6672921 4434418_-1299749_6672981 kind=MOON orbit=496 radius=0.4409925348108476 starId=-779017545 frame=false at=346248,0,52464 + body 4434347_-1299745_6672921 4434418_-1299749_6672981 kind=PLANET orbit=496 radius=1.528871001395831 starId=-779017545 frame=true at=0,0,0 + body 4544946_-354196_1918720 4544946_-354196_1918720 kind=MOON orbit=0 radius=0.48546330791476977 starId=-916256941 frame=false at=48021,0,-19950 + body 4544946_-354196_1918720 4544946_-354196_1918720 kind=ROGUE_PLANET orbit=0 radius=0.6109303737088227 starId=-916256941 frame=true at=0,0,0 + body 5314096_-799494_-3236895 5314096_-799494_-3236895 kind=MOON orbit=0 radius=1.8164474484584918 starId=-640125417 frame=false at=125521,0,125077 + body 5314096_-799494_-3236895 5314096_-799494_-3236895 kind=MOON orbit=0 radius=1.877907646816893 starId=-640125417 frame=false at=-174768,0,101691 + body 5314096_-799494_-3236895 5314096_-799494_-3236895 kind=ROGUE_PLANET orbit=0 radius=0.7834351676919846 starId=-640125417 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400307_5307314_4625912 kind=MOON orbit=479 radius=0.36600502259152407 starId=-1596760973 frame=false at=185186,0,-188155 + body 5400348_5307314_4625833 5400307_5307314_4625912 kind=MOON orbit=479 radius=0.5353292044399672 starId=-1596760973 frame=false at=363113,0,292070 + body 5400348_5307314_4625833 5400307_5307314_4625912 kind=PLANET orbit=479 radius=2.0426135503873906 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400344_5307314_4625827 kind=PLANET orbit=40 radius=1.7687375272010322 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400344_5307314_4625828 kind=ASTEROID_BELT orbit=37 radius=0.0 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400344_5307314_4625834 kind=PLANET orbit=23 radius=1.8495286024364148 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400346_5307314_4625835 kind=MOON orbit=15 radius=0.5844551676721068 starId=-1596760973 frame=false at=4672,0,27405 + body 5400348_5307314_4625833 5400346_5307314_4625835 kind=PLANET orbit=15 radius=0.3309110169492018 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400348_5307314_4625833 kind=STAR orbit=0 radius=0.0 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400349_5307314_4625833 kind=MOON orbit=7 radius=0.5136895480255286 starId=-1596760973 frame=false at=-36569,0,-60157 + body 5400348_5307314_4625833 5400349_5307314_4625833 kind=MOON orbit=7 radius=0.6551993518714251 starId=-1596760973 frame=false at=19338,0,128351 + body 5400348_5307314_4625833 5400349_5307314_4625833 kind=PLANET orbit=7 radius=0.4631510958156348 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400357_5307313_4625779 kind=MOON orbit=292 radius=0.507106136396319 starId=-1596760973 frame=false at=-50023,0,-107314 + body 5400348_5307314_4625833 5400357_5307313_4625779 kind=MOON orbit=292 radius=0.5178855145286163 starId=-1596760973 frame=false at=28458,0,24267 + body 5400348_5307314_4625833 5400357_5307313_4625779 kind=PLANET orbit=292 radius=0.45405883853013485 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400359_5307314_4625839 kind=GAS_GIANT orbit=67 radius=5.964991020215376 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400368_5307314_4625842 kind=MOON orbit=119 radius=0.46433487866080986 starId=-1596760973 frame=false at=6216,0,-83770 + body 5400348_5307314_4625833 5400368_5307314_4625842 kind=PLANET orbit=119 radius=0.5261890870626688 starId=-1596760973 frame=true at=0,0,0 + body 5400348_5307314_4625833 5400486_5307316_4625794 kind=ASTEROID_BELT orbit=766 radius=0.0 starId=-1596760973 frame=true at=0,0,0 + body 5482981_6750208_3250872 5482944_6750208_3250841 kind=STAR orbit=257 radius=75.38633159816266 starId=-512586094 frame=true at=0,0,0 + body 5482981_6750208_3250872 5482961_6750209_3250857 kind=ASTEROID_BELT orbit=134 radius=0.0 starId=-512586093 frame=true at=0,0,0 + body 5482981_6750208_3250872 5482970_6750209_3250861 kind=PLANET orbit=84 radius=0.511386558342394 starId=-512586093 frame=true at=0,0,0 + body 5482981_6750208_3250872 5482979_6750208_3250872 kind=PLANET orbit=10 radius=2.0786570647083242 starId=-512586093 frame=true at=0,0,0 + body 5482981_6750208_3250872 5482981_6750208_3250872 kind=STAR orbit=0 radius=0.0 starId=-512586093 frame=true at=0,0,0 + body 5482981_6750208_3250872 5482982_6750208_3250876 kind=PLANET orbit=23 radius=0.3395689288691417 starId=-512586093 frame=true at=0,0,0 + body 569093_2111063_3969013 569093_2111063_3969013 kind=ROGUE_PLANET orbit=0 radius=0.6864916344948702 starId=-852908577 frame=true at=0,0,0 + body 5697881_301001_4979351 5697881_301001_4979351 kind=MOON orbit=0 radius=2.14719261106477 starId=-945380041 frame=false at=152014,0,122135 + body 5697881_301001_4979351 5697881_301001_4979351 kind=ROGUE_PLANET orbit=0 radius=0.6458161630069941 starId=-945380041 frame=true at=0,0,0 + body 6696574_1162321_-1072222 6696574_1162321_-1072222 kind=ROGUE_PLANET orbit=0 radius=1.2078455531653214 starId=-636290765 frame=true at=0,0,0 + body 6807485_4657985_-295159 6807485_4657985_-295159 kind=MOON orbit=0 radius=0.6561439936700475 starId=-862242349 frame=false at=31412,0,331113 + body 6807485_4657985_-295159 6807485_4657985_-295159 kind=MOON orbit=0 radius=0.7067552937739463 starId=-862242349 frame=false at=-107171,0,-266239 + body 6807485_4657985_-295159 6807485_4657985_-295159 kind=ROGUE_PLANET orbit=0 radius=1.1260629550442611 starId=-862242349 frame=true at=0,0,0 + derived -1230314_1893274_-2189019 -1230314_1893274_-2189019 type=barren mass=0.01555362067257163 radius=0.3271755891768876 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=71697 metallicity=1.2624377715010906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1735123_-1297562_-3230849 -1735123_-1297562_-3230849 type=ice mass=0.8667932843771754 radius=0.9122063191004484 gravity=104 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=21616 metallicity=0.5277737560293576 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1839127_-930469_3024425 -1839127_-930469_3024425 type=ice mass=2.4824153191183425 radius=1.2352684367448383 gravity=163 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=15461 metallicity=0.4548654886227668 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2967914_5695077_-2605367 type=gasgiant mass=66.67616439538739 radius=5.5771406010519655 gravity=214 pressure=1600 tempK=70 oxygen=false locked=false rings=true rotation=5084 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968046_5695074_-2605415 type=ice mass=1.2526682385003438 radius=1.0703624393599125 gravity=109 pressure=1600 tempK=127 oxygen=false locked=false rings=false rotation=14288 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968054_5695071_-2605430 type=ice mass=1.5204363781222705 radius=1.1112601957037147 gravity=123 pressure=1600 tempK=159 oxygen=false locked=false rings=false rotation=32177 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968066_5695072_-2605456 type=gasgiant mass=60.09856952315779 radius=5.330894440915554 gravity=211 pressure=1600 tempK=322 oxygen=false locked=false rings=false rotation=10205 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968066_5695072_-2605466 type=icegiant mass=52.653240129683724 radius=5.032996057500449 gravity=208 pressure=1600 tempK=239 oxygen=false locked=false rings=false rotation=10756 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968073_5695072_-2605452 type=greenhouse mass=26.85856394766772 radius=2.3001142453388033 gravity=400 pressure=1600 tempK=594 oxygen=false locked=true rings=false rotation=54735 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968074_5695072_-2605453 type=lava mass=1.3553886823435224 radius=1.1461775365084355 gravity=103 pressure=5 tempK=933 oxygen=false locked=true rings=false rotation=29885 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968075_5695072_-2605459 type=barren mass=0.10017701075525987 radius=0.5085798172977589 gravity=39 pressure=4 tempK=195 oxygen=false locked=true rings=false rotation=76477 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968080_5695072_-2605444 type=exotic mass=1.8070992624622861 radius=1.2180207924065842 gravity=122 pressure=1600 tempK=308 oxygen=false locked=false rings=false rotation=25228 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968141_5695075_-2605545 type=gasgiant mass=96.83774537928785 radius=6.559618412306246 gravity=225 pressure=1600 tempK=87 oxygen=false locked=false rings=true rotation=10130 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2968074_5695072_-2605453 -2968198_5695072_-2605067 type=gasgiant mass=213.1692061439288 radius=9.244209790920415 gravity=249 pressure=1600 tempK=48 oxygen=false locked=false rings=true rotation=11019 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3037636_-286450_6061438 -3037636_-286450_6061438 type=ice mass=1.5263246991696868 radius=1.1321185551403936 gravity=119 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=26481 metallicity=0.6199677203655225 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3258554_1675500_3803298 -3258547_1675497_3803188 type=superearth mass=5.52311599764172 radius=1.6254072360065355 gravity=209 pressure=1600 tempK=83 oxygen=false locked=false rings=false rotation=39819 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3258554_1675500_3803298 -3258547_1675499_3803284 type=gasgiant mass=69.3043952234646 radius=5.6716793111039 gravity=215 pressure=1600 tempK=203 oxygen=false locked=false rings=true rotation=9067 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3258554_1675500_3803298 -3258554_1675500_3803298 type=barren mass=0.015484631073251742 radius=0.3089612503713161 gravity=16 pressure=0 tempK=954 oxygen=false locked=true rings=false rotation=30712 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3258554_1675500_3803298 -3258555_1675500_3803299 type=greenhouse mass=1.4526159580169788 radius=1.0933516689678018 gravity=122 pressure=305 tempK=366 oxygen=false locked=true rings=false rotation=42913 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3258554_1675500_3803298 -3258562_1675500_3803301 type=superearth mass=5.4324890429845 radius=1.5871017477083504 gravity=216 pressure=1600 tempK=299 oxygen=false locked=false rings=false rotation=10021 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3258554_1675500_3803298 -3258563_1675497_3803230 type=barren mass=0.004716973573928933 radius=0.22596366099897897 gravity=9 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=6688 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -545630_6755069_2561467 -545471_6755071_2561465 type=gasgiant mass=261.670585699835 radius=10.105985698063467 gravity=256 pressure=1600 tempK=119 oxygen=false locked=false rings=true rotation=7464 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -545630_6755069_2561467 -545611_6755062_2561721 type=ice mass=2.7508147708605315 radius=1.28776136557378 gravity=166 pressure=1600 tempK=89 oxygen=false locked=false rings=false rotation=16466 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -545630_6755069_2561467 -545624_6755069_2561468 type=greenhouse mass=1.051255092694872 radius=1.055941406990247 gravity=94 pressure=187 tempK=290 oxygen=false locked=true rings=false rotation=20156 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -545630_6755069_2561467 -545630_6755069_2561467 type=lava mass=0.3707594162294638 radius=0.8080341280862053 gravity=57 pressure=0 tempK=1751 oxygen=false locked=true rings=false rotation=47258 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -545630_6755069_2561467 -545634_6755067_2561379 type=icegiant mass=37.722267124329875 radius=4.353696915708663 gravity=199 pressure=1600 tempK=159 oxygen=false locked=false rings=false rotation=6909 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -545630_6755069_2561467 -545646_6755070_2561489 type=barren mass=0.009702196688479214 radius=0.2807380026340163 gravity=12 pressure=0 tempK=148 oxygen=false locked=false rings=false rotation=47061 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -545630_6755069_2561467 -545654_6755068_2561424 type=icegiant mass=106.5408516143004 radius=6.837693990559999 gravity=228 pressure=1600 tempK=214 oxygen=false locked=false rings=true rotation=5294 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -755250_5247165_5302015 -755250_5247165_5302015 type=ice mass=4.274166668479209 radius=1.5423697863264323 gravity=180 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=73337 metallicity=0.437474442147087 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -758984_1485681_3107299 -758984_1485681_3107299 type=superearth mass=33.05924302142529 radius=2.484840931637559 gravity=400 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=23269 metallicity=0.8875026328136386 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1159501_438377_2970042 1159501_438377_2970042 type=barren mass=0.0045062770008992325 radius=0.2438710079048173 gravity=8 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=24830 metallicity=0.4161491356171123 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1502575_5241619_578848 1502575_5241619_578848 type=ice mass=24.038493540644538 radius=2.2738202904677567 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=11883 metallicity=0.44569587581936077 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1915131_1673389_-1943074 1915131_1673389_-1943074 type=barren mass=0.08478477173619915 radius=0.5320514501890937 gravity=30 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=11849 metallicity=1.0785905850404696 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2639269_-1847617_-2599924 2639190_-1847611_-2600049 type=barren mass=0.017027347609292 radius=0.3444434132626236 gravity=14 pressure=2 tempK=61 oxygen=false locked=false rings=false rotation=11720 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2639269_-1847617_-2599924 2639267_-1847617_-2599925 type=desert mass=0.012680252203835457 radius=0.29987989003936566 gravity=14 pressure=0 tempK=275 oxygen=false locked=true rings=false rotation=11976 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2639269_-1847617_-2599924 2639269_-1847617_-2599924 type=barren mass=0.03122808828533539 radius=0.38794364457623254 gravity=21 pressure=0 tempK=954 oxygen=false locked=true rings=false rotation=46664 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2639269_-1847617_-2599924 2639272_-1847617_-2599928 type=barren mass=0.029935375461684652 radius=0.369897875724714 gravity=22 pressure=1 tempK=195 oxygen=false locked=true rings=false rotation=82646 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2639269_-1847617_-2599924 2639274_-1847617_-2599896 type=barren mass=0.045072349982484945 radius=0.42914253222295917 gravity=24 pressure=2 tempK=122 oxygen=false locked=false rings=false rotation=9301 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2639269_-1847617_-2599924 2639288_-1847617_-2599929 type=gasgiant mass=143.70130978715503 radius=7.78766563501808 gravity=237 pressure=1600 tempK=265 oxygen=false locked=false rings=false rotation=8848 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2639269_-1847617_-2599924 2639505_-1847606_-2599928 type=ice mass=25.37309969822193 radius=2.311518149114087 gravity=400 pressure=1600 tempK=90 oxygen=false locked=false rings=false rotation=14436 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2639269_-1847617_-2599924 2641577_-1847617_-2605840 type=ice mass=0.23217555047715116 radius=0.6570569261622582 gravity=54 pressure=474 tempK=13 oxygen=false locked=false rings=false rotation=20919 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2646744_5580083_-2997664 2646704_5580086_-2997730 type=ice mass=10.179842847683847 radius=1.792136039338186 gravity=317 pressure=1600 tempK=86 oxygen=false locked=false rings=false rotation=15461 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2646744_5580083_-2997664 2646744_5580083_-2997664 type=lava mass=25.33317144479418 radius=2.409159572567257 gravity=400 pressure=1600 tempK=2155 oxygen=false locked=true rings=false rotation=23474 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2646744_5580083_-2997664 2646746_5580083_-2997663 type=greenhouse mass=19.54000702893978 radius=2.3360233067337304 gravity=358 pressure=1600 tempK=433 oxygen=false locked=true rings=false rotation=8760 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2646744_5580083_-2997664 2646748_5580083_-2997658 type=superearth mass=6.889121985182743 radius=1.7389342248982458 gravity=228 pressure=1600 tempK=324 oxygen=false locked=true rings=false rotation=69188 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2646744_5580083_-2997664 2646762_5580082_-2997709 type=ice mass=0.0035763601522213286 radius=0.2127699482289579 gravity=8 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=22025 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2868006_4842033_4291324 2868006_4842033_4291324 type=ice mass=9.026670198345228 radius=1.7074633543397322 gravity=310 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=39771 metallicity=0.7832019177087589 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3210136_-1258479_1657446 3210128_-1258480_1657458 type=exotic mass=1.0668384556722796 radius=1.0175020841222762 gravity=103 pressure=1600 tempK=231 oxygen=false locked=false rings=false rotation=7932 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3210136_-1258479_1657446 3210136_-1258479_1657446 type=lava mass=0.14034585335322858 radius=0.5565614447937366 gravity=45 pressure=0 tempK=962 oxygen=false locked=true rings=false rotation=13092 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3210136_-1258479_1657446 3210136_-1258479_1657447 type=barren mass=0.1447581242742227 radius=0.5614017489351886 gravity=46 pressure=4 tempK=361 oxygen=false locked=true rings=false rotation=13189 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3210136_-1258479_1657446 3210142_-1258479_1657449 type=ocean mass=1.0852870404305224 radius=0.9817404084193058 gravity=113 pressure=390 tempK=257 oxygen=true locked=true rings=false rotation=19343 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3210136_-1258479_1657446 3210151_-1258481_1657352 type=gasgiant mass=23.98213586106359 radius=3.575461754416799 gravity=188 pressure=1600 tempK=83 oxygen=false locked=false rings=false rotation=10211 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3210136_-1258479_1657446 3210152_-1258477_1657503 type=superearth mass=10.039623894828077 radius=1.924905001225997 gravity=271 pressure=1600 tempK=114 oxygen=false locked=false rings=false rotation=17243 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3405269_-1627426_6553174 3405269_-1627426_6553174 type=ice mass=1.665831257268868 radius=1.0902957609031467 gravity=140 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=93192 metallicity=0.5227600746932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3866811_1951506_398707 type=ice mass=1.1759586814894833 radius=1.095697036970876 gravity=98 pressure=1600 tempK=6 oxygen=false locked=false rings=false rotation=75800 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3888302_1951507_402590 type=ice mass=0.586717705320025 radius=0.8164094286328627 gravity=88 pressure=1600 tempK=76 oxygen=false locked=false rings=false rotation=10881 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3888398_1951506_402530 type=gasgiant mass=162.41085724528673 radius=8.213303587243715 gravity=241 pressure=1600 tempK=281 oxygen=false locked=false rings=false rotation=14245 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3888405_1951506_402529 type=gasgiant mass=146.94126466276754 radius=7.863525818272932 gravity=238 pressure=1600 tempK=475 oxygen=false locked=false rings=true rotation=12031 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3888407_1951506_402529 type=desert mass=0.13526828875544214 radius=0.6096025245184175 gravity=36 pressure=2 tempK=316 oxygen=false locked=true rings=false rotation=8558 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3888408_1951506_402528 type=barren mass=0.044849501740026304 radius=0.45681884206073997 gravity=21 pressure=0 tempK=1060 oxygen=false locked=true rings=false rotation=22039 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3888408_1951506_402530 type=barren mass=0.04785511737114254 radius=0.4653093968013708 gravity=22 pressure=0 tempK=335 oxygen=false locked=true rings=false rotation=21716 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3888409_1951506_402514 type=gasgiant mass=92.93967973090871 radius=6.4434807244767 gravity=224 pressure=1600 tempK=240 oxygen=false locked=false rings=true rotation=9219 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3888419_1951505_402484 type=ice mass=13.869969155191379 radius=2.0003231974248514 gravity=347 pressure=1600 tempK=126 oxygen=false locked=false rings=false rotation=6262 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3888408_1951506_402528 3888426_1951506_402453 type=ice mass=24.646343623131614 radius=2.481279125837847 gravity=400 pressure=1600 tempK=96 oxygen=false locked=false rings=false rotation=14106 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4433045_-1299745_6671676 type=icegiant mass=274.59198337216975 radius=10.320006262012992 gravity=258 pressure=1600 tempK=22 oxygen=false locked=false rings=true rotation=5405 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434199_-1299751_6672916 type=ice mass=0.005088301601998299 radius=0.24188463795701431 gravity=9 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=20536 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434341_-1299745_6672928 type=exotic mass=1.1636338753392348 radius=1.029447635674642 gravity=110 pressure=1600 tempK=316 oxygen=false locked=true rings=false rotation=24767 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434347_-1299745_6672921 type=barren mass=0.0943230398749093 radius=0.5440812193502986 gravity=32 pressure=0 tempK=1033 oxygen=false locked=true rings=false rotation=6419 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434347_-1299745_6672926 type=barren mass=0.0966967689506941 radius=0.5488513114065555 gravity=32 pressure=2 tempK=191 oxygen=false locked=true rings=false rotation=6096 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434348_-1299745_6672921 type=exotic mass=2.0450559378617754 radius=1.1564058055441164 gravity=153 pressure=90 tempK=404 oxygen=false locked=true rings=false rotation=43546 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434349_-1299745_6672922 type=barren mass=0.1021600990316428 radius=0.5263349713162284 gravity=37 pressure=1 tempK=286 oxygen=false locked=true rings=false rotation=26236 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434355_-1299745_6672923 type=ice mass=0.004157776232024905 radius=0.24161793660069364 gravity=7 pressure=0 tempK=130 oxygen=false locked=true rings=false rotation=33046 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434356_-1299746_6672944 type=gasgiant mass=128.38949594196157 radius=7.415370376903965 gravity=233 pressure=1600 tempK=175 oxygen=false locked=false rings=false rotation=10714 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434361_-1299745_6672921 type=icegiant mass=297.0260422938207 radius=10.678467980226468 gravity=260 pressure=1600 tempK=230 oxygen=false locked=false rings=false rotation=13470 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434396_-1299748_6672946 type=gasgiant mass=187.13111039273838 radius=8.735151803026602 gravity=245 pressure=1600 tempK=117 oxygen=false locked=false rings=false rotation=13321 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4434347_-1299745_6672921 4434418_-1299749_6672981 type=superearth mass=5.744501687852009 radius=1.528871001395831 gravity=246 pressure=1600 tempK=98 oxygen=false locked=false rings=false rotation=10726 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4544946_-354196_1918720 4544946_-354196_1918720 type=barren mass=0.19972857631014948 radius=0.6109303737088227 gravity=54 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=19942 metallicity=0.7583475794397297 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5314096_-799494_-3236895 5314096_-799494_-3236895 type=barren mass=0.4590735898881811 radius=0.7834351676919846 gravity=75 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=38621 metallicity=1.405033353237831 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400307_5307314_4625912 type=superearth mass=13.341372461812089 radius=2.0426135503873906 gravity=320 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=12137 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400344_5307314_4625827 type=superearth mass=8.932627278838716 radius=1.7687375272010322 gravity=286 pressure=1600 tempK=331 oxygen=false locked=true rings=false rotation=8346 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400344_5307314_4625828 type=superearth mass=10.098916569236577 radius=1.8314307830807581 gravity=301 pressure=1600 tempK=344 oxygen=false locked=true rings=false rotation=8044 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400344_5307314_4625834 type=greenhouse mass=10.447232336444829 radius=1.8495286024364148 gravity=305 pressure=1600 tempK=338 oxygen=false locked=true rings=false rotation=8919 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400346_5307314_4625835 type=barren mass=0.01609865061852627 radius=0.3309110169492018 gravity=15 pressure=0 tempK=254 oxygen=false locked=true rings=false rotation=9851 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400348_5307314_4625833 type=lava mass=7.099280932494793 radius=1.7755434460175963 gravity=225 pressure=229 tempK=1373 oxygen=false locked=true rings=false rotation=40861 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400349_5307314_4625833 type=barren mass=0.050951093437401285 radius=0.4631510958156348 gravity=24 pressure=0 tempK=373 oxygen=false locked=true rings=false rotation=11012 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400357_5307313_4625779 type=ice mass=0.055267288021912254 radius=0.45405883853013485 gravity=27 pressure=53 tempK=47 oxygen=false locked=false rings=false rotation=18799 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400359_5307314_4625839 type=gasgiant mass=77.82631559060034 radius=5.964991020215376 gravity=219 pressure=1600 tempK=235 oxygen=false locked=false rings=true rotation=7819 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400368_5307314_4625842 type=barren mass=0.07762502485035572 radius=0.5261890870626688 gravity=28 pressure=9 tempK=90 oxygen=false locked=false rings=false rotation=89423 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5400348_5307314_4625833 5400486_5307316_4625794 type=ice mass=0.09754862785267554 radius=0.5276029785816756 gravity=35 pressure=125 tempK=34 oxygen=false locked=false rings=false rotation=90122 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5482981_6750208_3250872 5482944_6750208_3250841 type=ice mass=9.876000388503094 radius=1.830097901279705 gravity=295 pressure=1600 tempK=131 oxygen=false locked=false rings=false rotation=16661 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5482981_6750208_3250872 5482961_6750209_3250857 type=superearth mass=17.871248045341385 radius=2.0729948379254326 gravity=400 pressure=1600 tempK=202 oxygen=false locked=false rings=false rotation=81929 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5482981_6750208_3250872 5482970_6750209_3250861 type=ice mass=0.0640682306634592 radius=0.511386558342394 gravity=24 pressure=7 tempK=97 oxygen=false locked=false rings=true rotation=30074 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5482981_6750208_3250872 5482979_6750208_3250872 type=superearth mass=14.867434224085532 radius=2.0786570647083242 gravity=344 pressure=1600 tempK=724 oxygen=false locked=true rings=false rotation=21206 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5482981_6750208_3250872 5482981_6750208_3250872 type=lava mass=5.898210294400681 radius=1.684516598207336 gravity=208 pressure=189 tempK=1429 oxygen=false locked=true rings=false rotation=58676 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5482981_6750208_3250872 5482982_6750208_3250876 type=barren mass=0.01757119920794134 radius=0.3395689288691417 gravity=15 pressure=0 tempK=224 oxygen=false locked=true rings=false rotation=50463 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 569093_2111063_3969013 569093_2111063_3969013 type=ice mass=0.23275880995672615 radius=0.6864916344948702 gravity=49 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=12095 metallicity=1.2057393145105653 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5697881_301001_4979351 5697881_301001_4979351 type=barren mass=0.23499421065383816 radius=0.6458161630069941 gravity=56 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=12504 metallicity=0.7927308232109274 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6696574_1162321_-1072222 6696574_1162321_-1072222 type=ice mass=1.9750536926360533 radius=1.2078455531653214 gravity=135 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=8748 metallicity=0.6735222436120273 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6807485_4657985_-295159 6807485_4657985_-295159 type=ice mass=1.2829558170167987 radius=1.1260629550442611 gravity=101 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=33919 metallicity=1.0404619585816652 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1230314_1893274_-2189019 id=-1485753477 kind=ROGUE_PLANET name=PGR--3525313.0.-3525313 starless + system -1735123_-1297562_-3230849 id=-457888649 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless + system -1839127_-930469_3024425 id=-1123773025 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless + system -2968074_5695072_-2605453 id=-423794625 kind=STAR name=PGS--3525313.3525313.-3525313 starTemp=40 starSize=0.7281860113143921 + system -3037636_-286450_6061438 id=-772485517 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless + system -3258554_1675500_3803298 id=-472927549 kind=STAR name=PGS--3525313.0.3525313 starTemp=40 starSize=0.7763141989707947 + system -545630_6755069_2561467 id=-1674485141 kind=STAR name=PGS--3525313.3525313.0 starTemp=70 starSize=0.8436675667762756 + system -755250_5247165_5302015 id=-157245369 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system -758984_1485681_3107299 id=-726144233 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless + system 1159501_438377_2970042 id=-875095761 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 1502575_5241619_578848 id=-440778097 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless + system 1915131_1673389_-1943074 id=-268045669 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless + system 2639269_-1847617_-2599924 id=-1586685861 kind=STAR name=PGS-0.-3525313.-3525313 starTemp=40 starSize=0.7759418487548828 + system 2646744_5580083_-2997664 id=-1182950741 kind=STAR name=PGS-0.3525313.-3525313 starTemp=40 starSize=0.7727809548377991 + system 2868006_4842033_4291324 id=-1484690657 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless + system 3210136_-1258479_1657446 id=-143401165 kind=STAR name=PGS-0.-3525313.0 starTemp=40 starSize=0.7805290818214417 + system 3405269_-1627426_6553174 id=-1023282445 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless + system 3888408_1951506_402528 id=-1169505181 kind=STAR name=PGS-3525313.0.0 starTemp=40 starSize=0.9578028321266174 + system 4434347_-1299745_6672921 id=-779017545 kind=STAR name=PGS-3525313.-3525313.3525313 starTemp=40 starSize=0.9092349410057068 + system 4544946_-354196_1918720 id=-916256941 kind=ROGUE_PLANET name=PGR-3525313.-3525313.0 starless + system 5314096_-799494_-3236895 id=-640125417 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless + system 5400348_5307314_4625833 id=-1596760973 kind=STAR name=PGS-3525313.3525313.3525313 starTemp=40 starSize=0.8300331234931946 + system 5482981_6750208_3250872 id=-512586093 kind=STAR name=PGS-3525313.3525313.0 starTemp=40 starSize=0.9896072149276733 + system 569093_2111063_3969013 id=-852908577 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless + system 5697881_301001_4979351 id=-945380041 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless + system 6696574_1162321_-1072222 id=-636290765 kind=ROGUE_PLANET name=PGR-3525313.0.-3525313 starless + system 6807485_4657985_-295159 id=-862242349 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless seed 42 systems=27 - body -147990_-4259982_-3782514 -147990_-4259982_-3782514 kind=MOON orbit=0 radius=1.8658608999383741 starId=-1021692653 frame=false - body -147990_-4259982_-3782514 -147990_-4259982_-3782514 kind=ROGUE_PLANET orbit=0 radius=0.2447237286529341 starId=-1021692653 frame=true - body -1763249_4241372_4386755 -1763249_4241372_4386755 kind=ROGUE_PLANET orbit=0 radius=0.32767939488983133 starId=-1507112769 frame=true - body -1811609_-4167359_4728900 -1811609_-4167359_4728900 kind=MOON orbit=0 radius=0.8636213541980484 starId=-1319680873 frame=false - body -1811609_-4167359_4728900 -1811609_-4167359_4728900 kind=MOON orbit=0 radius=1.3583852306221664 starId=-1319680873 frame=false - body -1811609_-4167359_4728900 -1811609_-4167359_4728900 kind=ROGUE_PLANET orbit=0 radius=1.8824394321228168 starId=-1319680873 frame=true - body -2114326_9425508_6200501 -2114326_9425508_6200501 kind=ROGUE_PLANET orbit=0 radius=1.0893681258617856 starId=-525238425 frame=true - body -2550052_6994691_-1786506 -2550052_6994691_-1786506 kind=ROGUE_PLANET orbit=0 radius=0.32882272714498073 starId=-1940610717 frame=true - body -2559146_-594813_8660842 -2559146_-594813_8660842 kind=ROGUE_PLANET orbit=0 radius=1.2175511349308201 starId=-322493161 frame=true - body -3234634_7878403_530638 -3234634_7878403_530638 kind=MOON orbit=0 radius=0.5704338346689674 starId=-782417921 frame=false - body -3234634_7878403_530638 -3234634_7878403_530638 kind=ROGUE_PLANET orbit=0 radius=1.3180441026495238 starId=-782417921 frame=true - body -4729070_1810660_-2040913 -4729070_1810660_-2040913 kind=ROGUE_PLANET orbit=0 radius=1.2653982081483837 starId=-1026598645 frame=true - body -517007_1928357_7786828 -517007_1928357_7786828 kind=ROGUE_PLANET orbit=0 radius=1.2632316197428803 starId=-819120817 frame=true - body 2446288_9823217_1140272 2446221_9823217_1140222 kind=STAR orbit=445 radius=67.32509275317192 starId=-677694142 frame=true - body 2446288_9823217_1140272 2446277_9823217_1140255 kind=GAS_GIANT orbit=108 radius=7.526351165166979 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446277_9823217_1140255 kind=MOON orbit=108 radius=0.22381749339605467 starId=-677694141 frame=false - body 2446288_9823217_1140272 2446277_9823217_1140255 kind=MOON orbit=108 radius=0.5067698734179844 starId=-677694141 frame=false - body 2446288_9823217_1140272 2446282_9823217_1140274 kind=MOON orbit=37 radius=0.7409829178572391 starId=-677694141 frame=false - body 2446288_9823217_1140272 2446282_9823217_1140274 kind=PLANET orbit=37 radius=1.3635217085174838 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446285_9823217_1140269 kind=PLANET orbit=23 radius=1.3471617398696347 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446287_9823217_1140272 kind=PLANET orbit=7 radius=1.1015474620849677 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446288_9823217_1140272 kind=STAR orbit=0 radius=0.0 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446289_9823217_1140273 kind=PLANET orbit=10 radius=0.2092017435866524 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446289_9823217_1140275 kind=PLANET orbit=18 radius=0.2114444291012643 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446290_9823217_1140262 kind=PLANET orbit=57 radius=0.2281630648172823 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446292_9823217_1140264 kind=ASTEROID_BELT orbit=49 radius=0.0 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446298_9823215_1140303 kind=ASTEROID_BELT orbit=172 radius=0.0 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446305_9823217_1140274 kind=GAS_GIANT orbit=89 radius=6.153023641353679 starId=-677694141 frame=true - body 2446288_9823217_1140272 2446305_9823217_1140274 kind=MOON orbit=89 radius=0.24758179107202047 starId=-677694141 frame=false - body 2446288_9823217_1140272 2446305_9823217_1140274 kind=MOON orbit=89 radius=0.2828896289074508 starId=-677694141 frame=false - body 2446288_9823217_1140272 2446305_9823217_1140274 kind=MOON orbit=89 radius=0.3633104289540722 starId=-677694141 frame=false - body 2446288_9823217_1140272 2446305_9823217_1140274 kind=MOON orbit=89 radius=0.3801950767827389 starId=-677694141 frame=false - body 331018_-2627546_4002550 331018_-2627546_4002550 kind=ROGUE_PLANET orbit=0 radius=0.648198320022664 starId=-778284213 frame=true - body 3810943_7578176_-1529346 3810832_7578170_-1529377 kind=MOON orbit=617 radius=0.20784045627308434 starId=-1781882117 frame=false - body 3810943_7578176_-1529346 3810832_7578170_-1529377 kind=MOON orbit=617 radius=0.5341977665142178 starId=-1781882117 frame=false - body 3810943_7578176_-1529346 3810832_7578170_-1529377 kind=PLANET orbit=617 radius=0.21061448925959111 starId=-1781882117 frame=true - body 3810943_7578176_-1529346 3810930_7578178_-1529283 kind=PLANET orbit=343 radius=1.1579452921819666 starId=-1781882117 frame=true - body 3810943_7578176_-1529346 3810940_7578176_-1529345 kind=STAR orbit=14 radius=82.01533210158348 starId=-1781882118 frame=true - body 3810943_7578176_-1529346 3810943_7578176_-1529346 kind=STAR orbit=0 radius=0.0 starId=-1781882117 frame=true - body 3810943_7578176_-1529346 3810948_7578177_-1529360 kind=PLANET orbit=80 radius=2.1277124765728215 starId=-1781882117 frame=true - body 3810943_7578176_-1529346 3810969_7578175_-1529352 kind=PLANET orbit=145 radius=0.4201376811063558 starId=-1781882117 frame=true - body 3810943_7578176_-1529346 3811028_7578212_-1530104 kind=ASTEROID_BELT orbit=4083 radius=0.0 starId=-1781882117 frame=true - body 3810943_7578176_-1529346 3811141_7578179_-1529254 kind=PLANET orbit=1167 radius=0.3932058876865541 starId=-1781882117 frame=true - body 3810943_7578176_-1529346 3811237_7578161_-1529722 kind=MOON orbit=2552 radius=0.21212185397109995 starId=-1781882117 frame=false - body 3810943_7578176_-1529346 3811237_7578161_-1529722 kind=MOON orbit=2552 radius=0.3343998307038777 starId=-1781882117 frame=false - body 3810943_7578176_-1529346 3811237_7578161_-1529722 kind=PLANET orbit=2552 radius=1.6686795831939623 starId=-1781882117 frame=true - body 4285126_880860_5633403 4285059_880873_5632810 kind=ASTEROID_BELT orbit=3190 radius=0.0 starId=-1076361445 frame=true - body 4285126_880860_5633403 4285073_880861_5633289 kind=PLANET orbit=672 radius=1.128421875313703 starId=-1076361445 frame=true - body 4285126_880860_5633403 4285102_880861_5633396 kind=ASTEROID_BELT orbit=133 radius=0.0 starId=-1076361445 frame=true - body 4285126_880860_5633403 4285109_880858_5633362 kind=GAS_GIANT orbit=240 radius=3.854805170021379 starId=-1076361445 frame=true - body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.20025483714909595 starId=-1076361445 frame=false - body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.2359910359192303 starId=-1076361445 frame=false - body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.3360418801675671 starId=-1076361445 frame=false - body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.3582314708170581 starId=-1076361445 frame=false - body 4285126_880860_5633403 4285109_880858_5633362 kind=MOON orbit=240 radius=0.7068934191067848 starId=-1076361445 frame=false - body 4285126_880860_5633403 4285126_880860_5633403 kind=STAR orbit=0 radius=0.0 starId=-1076361445 frame=true - body 4285126_880860_5633403 4285126_880860_5633405 kind=STAR orbit=9 radius=101.9993340432644 starId=-1076361446 frame=true - body 4285126_880860_5633403 4285131_880860_5633401 kind=PLANET orbit=30 radius=2.0812185825857674 starId=-1076361445 frame=true - body 4285126_880860_5633403 4285136_880859_5633399 kind=MOON orbit=57 radius=0.22397767735041765 starId=-1076361445 frame=false - body 4285126_880860_5633403 4285136_880859_5633399 kind=MOON orbit=57 radius=0.3211331002221319 starId=-1076361445 frame=false - body 4285126_880860_5633403 4285136_880859_5633399 kind=PLANET orbit=57 radius=1.401970044249525 starId=-1076361445 frame=true - body 4285126_880860_5633403 4285497_880855_5633365 kind=PLANET orbit=1994 radius=0.4319970136446857 starId=-1076361445 frame=true - body 4287194_-1179392_-2651040 4287194_-1179392_-2651040 kind=MOON orbit=0 radius=1.7342488163425542 starId=-493026021 frame=false - body 4287194_-1179392_-2651040 4287194_-1179392_-2651040 kind=ROGUE_PLANET orbit=0 radius=0.5675357443169351 starId=-493026021 frame=true - body 4540244_-2195284_6241886 4540243_-2195284_6241883 kind=PLANET orbit=15 radius=0.35933183620673187 starId=-1311232065 frame=true - body 4540244_-2195284_6241886 4540244_-2195284_6241886 kind=STAR orbit=0 radius=0.0 starId=-1311232065 frame=true - body 4540244_-2195284_6241886 4540250_-2195286_6241917 kind=PLANET orbit=167 radius=0.525683512535432 starId=-1311232065 frame=true - body 4540244_-2195284_6241886 4540258_-2195290_6241733 kind=PLANET orbit=820 radius=0.44478714057429125 starId=-1311232065 frame=true - body 4540244_-2195284_6241886 4540402_-2195292_6242073 kind=ASTEROID_BELT orbit=1312 radius=0.0 starId=-1311232065 frame=true - body 548615_3409698_-890024 548615_3409698_-890024 kind=MOON orbit=0 radius=1.0180941946589208 starId=-502144617 frame=false - body 548615_3409698_-890024 548615_3409698_-890024 kind=MOON orbit=0 radius=2.025383247329257 starId=-502144617 frame=false - body 548615_3409698_-890024 548615_3409698_-890024 kind=ROGUE_PLANET orbit=0 radius=0.9755138376471226 starId=-502144617 frame=true - body 5659038_1332946_1424772 5658972_1332942_1424881 kind=ASTEROID_BELT orbit=681 radius=0.0 starId=-1094984105 frame=true - body 5659038_1332946_1424772 5659002_1332946_1424807 kind=STAR orbit=265 radius=106.81875300943851 starId=-1094984106 frame=true - body 5659038_1332946_1424772 5659031_1332945_1424786 kind=PLANET orbit=84 radius=2.3821474285420066 starId=-1094984105 frame=true - body 5659038_1332946_1424772 5659038_1332946_1424767 kind=MOON orbit=28 radius=0.26461678783224585 starId=-1094984105 frame=false - body 5659038_1332946_1424772 5659038_1332946_1424767 kind=MOON orbit=28 radius=0.5694454525567392 starId=-1094984105 frame=false - body 5659038_1332946_1424772 5659038_1332946_1424767 kind=PLANET orbit=28 radius=0.5652156493519473 starId=-1094984105 frame=true - body 5659038_1332946_1424772 5659038_1332946_1424772 kind=STAR orbit=0 radius=0.0 starId=-1094984105 frame=true - body 5659038_1332946_1424772 5659146_1332945_1424974 kind=GAS_GIANT orbit=1226 radius=10.26830810198086 starId=-1094984105 frame=true - body 5659038_1332946_1424772 5659218_1332958_1425091 kind=ASTEROID_BELT orbit=1961 radius=0.0 starId=-1094984105 frame=true - body 6253955_-322304_1235104 6253955_-322304_1235104 kind=MOON orbit=0 radius=4.6400363601629975 starId=-1379053845 frame=false - body 6253955_-322304_1235104 6253955_-322304_1235104 kind=ROGUE_PLANET orbit=0 radius=0.4073729049004505 starId=-1379053845 frame=true - body 6326499_-4297175_-3049933 6326499_-4297175_-3049933 kind=ROGUE_PLANET orbit=0 radius=0.23300568312102693 starId=-335655149 frame=true - body 7518342_4618068_5122633 7518342_4618068_5122633 kind=ROGUE_PLANET orbit=0 radius=0.3709252755727711 starId=-1527090629 frame=true - body 783965_8560023_8900358 783965_8560023_8900358 kind=ROGUE_PLANET orbit=0 radius=1.2840791507608935 starId=-602652429 frame=true - body 8253893_-3146511_6379169 8253883_-3146511_6384549 kind=STAR orbit=28770 radius=73.8964213693142 starId=-1913306834 frame=true - body 8253893_-3146511_6379169 8253893_-3146511_6379169 kind=STAR orbit=0 radius=0.0 starId=-1913306833 frame=true - body 8253893_-3146511_6379169 8253895_-3146511_6379168 kind=PLANET orbit=13 radius=0.35018276058976605 starId=-1913306833 frame=true - body 8253893_-3146511_6379169 8253899_-3146511_6379172 kind=PLANET orbit=38 radius=1.9849839958996087 starId=-1913306833 frame=true - body 8253893_-3146511_6379169 8253909_-3146510_6379156 kind=PLANET orbit=112 radius=0.28522355165984403 starId=-1913306833 frame=true - body 8253893_-3146511_6379169 8253942_-3146510_6379142 kind=PLANET orbit=298 radius=2.1533492603769537 starId=-1913306833 frame=true - body 8253893_-3146511_6379169 8253955_-3146511_6379105 kind=ASTEROID_BELT orbit=476 radius=0.0 starId=-1913306833 frame=true - body 8259566_6483340_6516020 8259566_6483340_6516020 kind=ROGUE_PLANET orbit=0 radius=0.29300354320951977 starId=-373298525 frame=true - body 8396159_3733749_-457386 8396120_3733751_-457350 kind=MOON orbit=284 radius=0.4924311613292982 starId=-1235930289 frame=false - body 8396159_3733749_-457386 8396120_3733751_-457350 kind=PLANET orbit=284 radius=2.2417612943334566 starId=-1235930289 frame=true - body 8396159_3733749_-457386 8396128_3733749_-457307 kind=ASTEROID_BELT orbit=454 radius=0.0 starId=-1235930289 frame=true - body 8396159_3733749_-457386 8396147_3733750_-457385 kind=MOON orbit=64 radius=0.20798542465380418 starId=-1235930289 frame=false - body 8396159_3733749_-457386 8396147_3733750_-457385 kind=MOON orbit=64 radius=0.2733252772924753 starId=-1235930289 frame=false - body 8396159_3733749_-457386 8396147_3733750_-457385 kind=PLANET orbit=64 radius=0.8297832210543126 starId=-1235930289 frame=true - body 8396159_3733749_-457386 8396157_3733749_-457386 kind=PLANET orbit=9 radius=1.3026390694134573 starId=-1235930289 frame=true - body 8396159_3733749_-457386 8396159_3733749_-457386 kind=STAR orbit=0 radius=0.0 starId=-1235930289 frame=true - body 8396159_3733749_-457386 8396159_3733749_-457389 kind=MOON orbit=15 radius=0.2690640072563959 starId=-1235930289 frame=false - body 8396159_3733749_-457386 8396159_3733749_-457389 kind=PLANET orbit=15 radius=0.5873692699834249 starId=-1235930289 frame=true - body 8396159_3733749_-457386 8396165_3733749_-457390 kind=PLANET orbit=39 radius=0.6440401650263456 starId=-1235930289 frame=true - body 8396159_3733749_-457386 8396168_3733749_-457421 kind=PLANET orbit=195 radius=0.9249787785253212 starId=-1235930289 frame=true - body 874922_4033773_3027920 874922_4033773_3027920 kind=MOON orbit=0 radius=0.5867444102269814 starId=-1958754413 frame=false - body 874922_4033773_3027920 874922_4033773_3027920 kind=MOON orbit=0 radius=0.6081141830622963 starId=-1958754413 frame=false - body 874922_4033773_3027920 874922_4033773_3027920 kind=ROGUE_PLANET orbit=0 radius=2.2898780119292534 starId=-1958754413 frame=true - body 9101293_8238273_-4074920 9096316_8238459_-4076773 kind=MOON orbit=28418 radius=0.34028850086578666 starId=-1857245365 frame=false - body 9101293_8238273_-4074920 9096316_8238459_-4076773 kind=PLANET orbit=28418 radius=0.6877040920784976 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9101053_8238297_-4074078 kind=GAS_GIANT orbit=4682 radius=8.602364603527008 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9101075_8238268_-4074884 kind=ASTEROID_BELT orbit=1182 radius=0.0 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=GAS_GIANT orbit=2128 radius=8.688024092877786 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=MOON orbit=2128 radius=0.4558770711816329 starId=-1857245365 frame=false - body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=MOON orbit=2128 radius=0.517815056947268 starId=-1857245365 frame=false - body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=MOON orbit=2128 radius=0.5991392207919304 starId=-1857245365 frame=false - body 9101293_8238273_-4074920 9101095_8238254_-4074575 kind=MOON orbit=2128 radius=0.6197041873623639 starId=-1857245365 frame=false - body 9101293_8238273_-4074920 9101152_8238265_-4074753 kind=PLANET orbit=1171 radius=0.6675340300021155 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9101287_8238269_-4075013 kind=PLANET orbit=498 radius=1.949315024881134 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9101293_8238273_-4074920 kind=STAR orbit=0 radius=0.0 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9101309_8238211_-4076205 kind=PLANET orbit=6879 radius=2.055381652866782 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9101335_8238275_-4074965 kind=MOON orbit=330 radius=0.25776475988841896 starId=-1857245365 frame=false - body 9101293_8238273_-4074920 9101335_8238275_-4074965 kind=PLANET orbit=330 radius=0.8648616554787374 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9101349_8238185_-4077785 kind=GAS_GIANT orbit=15329 radius=6.6090226899882865 starId=-1857245365 frame=true - body 9101293_8238273_-4074920 9102690_8238598_-4083301 kind=ASTEROID_BELT orbit=45468 radius=0.0 starId=-1857245365 frame=true - body 9115079_5410474_1795046 9115079_5410474_1795046 kind=MOON orbit=0 radius=1.3206635975936905 starId=-1757034753 frame=false - body 9115079_5410474_1795046 9115079_5410474_1795046 kind=ROGUE_PLANET orbit=0 radius=1.0266176771058164 starId=-1757034753 frame=true - derived -147990_-4259982_-3782514 -147990_-4259982_-3782514 type=ice mass=0.004767026424118472 radius=0.2447237286529341 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=16562 metallicity=0.6374424070421512 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1763249_4241372_4386755 -1763249_4241372_4386755 type=ice mass=0.017224606058141433 radius=0.32767939488983133 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=44247 metallicity=0.47420974986188824 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1811609_-4167359_4728900 -1811609_-4167359_4728900 type=superearth mass=12.019652275506662 radius=1.8824394321228168 gravity=339 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=79080 metallicity=1.0470297959069352 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2114326_9425508_6200501 -2114326_9425508_6200501 type=ice mass=1.2997261978222874 radius=1.0893681258617856 gravity=110 pressure=0 tempK=36 oxygen=false locked=false rings=true rotation=27748 metallicity=1.2451647291371057 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2550052_6994691_-1786506 -2550052_6994691_-1786506 type=barren mass=0.01658361521488155 radius=0.32882272714498073 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=19446 metallicity=0.8523999939023407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2559146_-594813_8660842 -2559146_-594813_8660842 type=ice mass=2.443441381145703 radius=1.2175511349308201 gravity=165 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=58446 metallicity=0.5373070678961682 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3234634_7878403_530638 -3234634_7878403_530638 type=ice mass=2.9790234699827027 radius=1.3180441026495238 gravity=171 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=14746 metallicity=0.760972477157181 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4729070_1810660_-2040913 -4729070_1810660_-2040913 type=ice mass=2.859837455249325 radius=1.2653982081483837 gravity=179 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=79465 metallicity=0.3681566576342936 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -517007_1928357_7786828 -517007_1928357_7786828 type=ice mass=2.1103637839767204 radius=1.2632316197428803 gravity=132 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=14112 metallicity=1.1055450947112693 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446221_9823217_1140222 type=icegiant mass=316.4815577035859 radius=10.977132265159819 gravity=263 pressure=1600 tempK=121 oxygen=false locked=false rings=true rotation=8336 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446277_9823217_1140255 type=gasgiant mass=132.85204474218668 radius=7.526351165166979 gravity=235 pressure=1600 tempK=177 oxygen=false locked=false rings=true rotation=13373 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446282_9823217_1140274 type=exotic mass=2.8042763914170217 radius=1.3635217085174838 gravity=151 pressure=1600 tempK=301 oxygen=false locked=true rings=false rotation=11035 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446285_9823217_1140269 type=greenhouse mass=2.9736945063864484 radius=1.3471617398696347 gravity=164 pressure=1600 tempK=293 oxygen=false locked=true rings=false rotation=18483 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446287_9823217_1140272 type=exotic mass=1.7747236936705673 radius=1.1015474620849677 gravity=146 pressure=125 tempK=361 oxygen=false locked=true rings=false rotation=43408 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446288_9823217_1140272 type=lava mass=22.49954928588334 radius=2.4811969807180527 gravity=365 pressure=1600 tempK=1925 oxygen=false locked=true rings=false rotation=58095 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446289_9823217_1140273 type=barren mass=0.0031867611215720924 radius=0.2092017435866524 gravity=7 pressure=0 tempK=269 oxygen=false locked=true rings=false rotation=9742 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446289_9823217_1140275 type=barren mass=0.0033234774498898385 radius=0.2114444291012643 gravity=7 pressure=0 tempK=201 oxygen=false locked=true rings=false rotation=9600 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446290_9823217_1140262 type=ice mass=0.0031894721532622593 radius=0.2281630648172823 gravity=6 pressure=0 tempK=95 oxygen=false locked=false rings=false rotation=8365 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446292_9823217_1140264 type=exotic mass=6.084905877266164 radius=1.6741507494893029 gravity=217 pressure=1600 tempK=265 oxygen=false locked=false rings=false rotation=7987 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446298_9823215_1140303 type=ice mass=0.06887354574092128 radius=0.4706609388538775 gravity=31 pressure=46 tempK=65 oxygen=false locked=false rings=false rotation=12192 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2446288_9823217_1140272 2446305_9823217_1140274 type=gasgiant mass=83.58487296657829 radius=6.153023641353679 gravity=221 pressure=1600 tempK=190 oxygen=false locked=false rings=false rotation=8617 metallicity=1.592616956127244 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 331018_-2627546_4002550 331018_-2627546_4002550 type=ice mass=0.19202465319892836 radius=0.648198320022664 gravity=46 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=9798 metallicity=0.4262158195671292 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3810943_7578176_-1529346 3810832_7578170_-1529377 type=ice mass=0.0032286372068856587 radius=0.21061448925959111 gravity=7 pressure=0 tempK=78 oxygen=false locked=false rings=false rotation=10609 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3810943_7578176_-1529346 3810930_7578178_-1529283 type=exotic mass=1.5221744884553101 radius=1.1579452921819666 gravity=114 pressure=1600 tempK=270 oxygen=false locked=false rings=false rotation=6121 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3810943_7578176_-1529346 3810940_7578176_-1529345 type=barren mass=0.0029832573614254915 radius=0.2158682404627622 gravity=6 pressure=0 tempK=534 oxygen=false locked=true rings=false rotation=11396 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3810943_7578176_-1529346 3810943_7578176_-1529346 type=barren mass=0.040851204012771695 radius=0.42132762762736997 gravity=23 pressure=0 tempK=982 oxygen=false locked=true rings=false rotation=41772 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3810943_7578176_-1529346 3810948_7578177_-1529360 type=superearth mass=16.848537562523344 radius=2.1277124765728215 gravity=372 pressure=1600 tempK=557 oxygen=false locked=false rings=false rotation=26542 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3810943_7578176_-1529346 3810969_7578175_-1529352 type=barren mass=0.04553182766757045 radius=0.4201376811063558 gravity=26 pressure=2 tempK=195 oxygen=false locked=false rings=false rotation=69585 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3810943_7578176_-1529346 3811028_7578212_-1530104 type=ice mass=1.091203688879023 radius=0.9816292025981661 gravity=113 pressure=1600 tempK=68 oxygen=false locked=false rings=false rotation=84653 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3810943_7578176_-1529346 3811141_7578179_-1529254 type=ice mass=0.03591972812186142 radius=0.3932058876865541 gravity=23 pressure=11 tempK=56 oxygen=false locked=false rings=false rotation=7128 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3810943_7578176_-1529346 3811237_7578161_-1529722 type=ice mass=6.47400061340064 radius=1.6686795831939623 gravity=233 pressure=1600 tempK=86 oxygen=false locked=false rings=false rotation=44932 metallicity=0.45750492565839407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4285126_880860_5633403 4285059_880873_5632810 type=icegiant mass=24.78871662757518 radius=3.6272568021231066 gravity=188 pressure=1600 tempK=75 oxygen=false locked=false rings=true rotation=7544 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4285126_880860_5633403 4285073_880861_5633289 type=ice mass=1.6015171943747801 radius=1.128421875313703 gravity=126 pressure=1600 tempK=155 oxygen=false locked=false rings=false rotation=61867 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4285126_880860_5633403 4285102_880861_5633396 type=barren mass=0.15881044989458584 radius=0.5918769282708047 gravity=45 pressure=21 tempK=188 oxygen=false locked=false rings=false rotation=31692 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4285126_880860_5633403 4285109_880858_5633362 type=gasgiant mass=28.512121925562884 radius=3.854805170021379 gravity=192 pressure=1600 tempK=274 oxygen=false locked=false rings=true rotation=13921 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4285126_880860_5633403 4285126_880860_5633403 type=lava mass=19.30475444324071 radius=2.131253834451699 gravity=400 pressure=485 tempK=3086 oxygen=false locked=true rings=false rotation=20687 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4285126_880860_5633403 4285126_880860_5633405 type=lava mass=18.538429815154963 radius=2.1119381062001854 gravity=400 pressure=1600 tempK=1529 oxygen=false locked=true rings=false rotation=21622 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4285126_880860_5633403 4285131_880860_5633401 type=lava mass=17.701478105223508 radius=2.0812185825857674 gravity=400 pressure=1600 tempK=891 oxygen=false locked=true rings=false rotation=79691 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4285126_880860_5633403 4285136_880859_5633399 type=superearth mass=3.890518639874639 radius=1.401970044249525 gravity=198 pressure=735 tempK=503 oxygen=false locked=false rings=false rotation=66507 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4285126_880860_5633403 4285497_880855_5633365 type=barren mass=0.046542099334781754 radius=0.4319970136446857 gravity=25 pressure=6 tempK=48 oxygen=false locked=false rings=false rotation=48150 metallicity=0.813915338547309 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4287194_-1179392_-2651040 4287194_-1179392_-2651040 type=barren mass=0.10833361309522743 radius=0.5675357443169351 gravity=34 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=43418 metallicity=1.4362747689344162 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4540244_-2195284_6241886 4540243_-2195284_6241883 type=barren mass=0.02034697498380806 radius=0.35933183620673187 gravity=16 pressure=0 tempK=473 oxygen=false locked=true rings=false rotation=14054 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4540244_-2195284_6241886 4540244_-2195284_6241886 type=lava mass=0.11540333451517869 radius=0.5678878284656146 gravity=36 pressure=0 tempK=1842 oxygen=false locked=true rings=false rotation=34812 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4540244_-2195284_6241886 4540250_-2195286_6241917 type=barren mass=0.079846587952403 radius=0.525683512535432 gravity=29 pressure=9 tempK=141 oxygen=false locked=false rings=false rotation=25667 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4540244_-2195284_6241886 4540258_-2195290_6241733 type=ice mass=0.05263347777872866 radius=0.44478714057429125 gravity=27 pressure=23 tempK=52 oxygen=false locked=false rings=false rotation=53953 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4540244_-2195284_6241886 4540402_-2195292_6242073 type=ice mass=1.137232749072275 radius=1.0699392330221018 gravity=99 pressure=1600 tempK=93 oxygen=false locked=false rings=false rotation=20557 metallicity=1.2674717954166617 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 548615_3409698_-890024 548615_3409698_-890024 type=ice mass=0.8316322455580678 radius=0.9755138376471226 gravity=87 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=51282 metallicity=0.41829481403499663 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5659038_1332946_1424772 5658972_1332942_1424881 type=gasgiant mass=153.02064266490837 radius=8.003357331978748 gravity=239 pressure=1600 tempK=144 oxygen=false locked=false rings=true rotation=7155 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5659038_1332946_1424772 5659002_1332946_1424807 type=superearth mass=6.594081074411881 radius=1.680261648083262 gravity=234 pressure=1600 tempK=249 oxygen=false locked=false rings=false rotation=13964 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5659038_1332946_1424772 5659031_1332945_1424786 type=greenhouse mass=19.690248030506307 radius=2.3821474285420066 gravity=347 pressure=1600 tempK=339 oxygen=false locked=false rings=false rotation=6424 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5659038_1332946_1424772 5659038_1332946_1424767 type=desert mass=0.15059433565894353 radius=0.5652156493519473 gravity=47 pressure=5 tempK=337 oxygen=false locked=true rings=false rotation=10188 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5659038_1332946_1424772 5659038_1332946_1424772 type=lava mass=3.3653614720096416 radius=1.3993165606585976 gravity=172 pressure=16 tempK=1901 oxygen=false locked=true rings=false rotation=29675 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5659038_1332946_1424772 5659146_1332945_1424974 type=gasgiant mass=271.4384670839881 radius=10.26830810198086 gravity=257 pressure=1600 tempK=108 oxygen=false locked=false rings=true rotation=8668 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5659038_1332946_1424772 5659218_1332958_1425091 type=ice mass=2.047358782492318 radius=1.2985499750340264 gravity=121 pressure=1600 tempK=80 oxygen=false locked=false rings=false rotation=6419 metallicity=0.9770163067766604 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6253955_-322304_1235104 6253955_-322304_1235104 type=barren mass=0.044689233799970744 radius=0.4073729049004505 gravity=27 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=35413 metallicity=0.7485853832713572 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6326499_-4297175_-3049933 6326499_-4297175_-3049933 type=barren mass=0.0051146307901779745 radius=0.23300568312102693 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=16827 metallicity=0.5074826406907766 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7518342_4618068_5122633 7518342_4618068_5122633 type=ice mass=0.021854390659360623 radius=0.3709252755727711 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=33784 metallicity=0.6229085872163995 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 783965_8560023_8900358 783965_8560023_8900358 type=ice mass=2.0737223615710203 radius=1.2840791507608935 gravity=126 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=76518 metallicity=1.5884364730460865 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8253893_-3146511_6379169 8253883_-3146511_6384549 type=ice mass=0.2344219835904629 radius=0.687466425152063 gravity=50 pressure=466 tempK=7 oxygen=false locked=false rings=false rotation=37472 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8253893_-3146511_6379169 8253893_-3146511_6379169 type=lava mass=19.104993593296804 radius=2.3010202631136 gravity=361 pressure=572 tempK=1559 oxygen=false locked=true rings=false rotation=10571 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8253893_-3146511_6379169 8253895_-3146511_6379168 type=barren mass=0.025566217269380617 radius=0.35018276058976605 gravity=21 pressure=0 tempK=247 oxygen=false locked=true rings=false rotation=44160 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8253893_-3146511_6379169 8253899_-3146511_6379172 type=superearth mass=12.683388525114609 radius=1.9849839958996087 gravity=322 pressure=1600 tempK=307 oxygen=false locked=true rings=false rotation=10455 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8253893_-3146511_6379169 8253909_-3146510_6379156 type=barren mass=0.011087535052431922 radius=0.28522355165984403 gravity=14 pressure=1 tempK=84 oxygen=false locked=false rings=false rotation=19148 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8253893_-3146511_6379169 8253942_-3146510_6379142 type=superearth mass=21.05654269918682 radius=2.1533492603769537 gravity=400 pressure=1600 tempK=109 oxygen=false locked=false rings=false rotation=14630 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8253893_-3146511_6379169 8253955_-3146511_6379105 type=ice mass=1.012653887217379 radius=0.9682541436385077 gravity=108 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=13322 metallicity=0.4379862938245407 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8259566_6483340_6516020 8259566_6483340_6516020 type=barren mass=0.012722502496168983 radius=0.29300354320951977 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=14151 metallicity=1.5687224409653222 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8396159_3733749_-457386 8396120_3733751_-457350 type=superearth mass=23.286398057466076 radius=2.2417612943334566 gravity=400 pressure=1600 tempK=117 oxygen=false locked=false rings=false rotation=7102 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8396159_3733749_-457386 8396128_3733749_-457307 type=superearth mass=9.812214870978613 radius=1.8690561684738647 gravity=281 pressure=1600 tempK=93 oxygen=false locked=false rings=false rotation=11719 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8396159_3733749_-457386 8396147_3733750_-457385 type=ice mass=0.5643326340921984 radius=0.8297832210543126 gravity=82 pressure=830 tempK=183 oxygen=false locked=false rings=false rotation=8552 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8396159_3733749_-457386 8396157_3733749_-457386 type=greenhouse mass=2.679147391530122 radius=1.3026390694134573 gravity=158 pressure=734 tempK=421 oxygen=false locked=true rings=false rotation=13905 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8396159_3733749_-457386 8396159_3733749_-457386 type=barren mass=0.17280756683991655 radius=0.6024715626687684 gravity=48 pressure=0 tempK=935 oxygen=false locked=true rings=false rotation=53296 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8396159_3733749_-457386 8396159_3733749_-457389 type=barren mass=0.15638614311529456 radius=0.5873692699834249 gravity=45 pressure=2 tempK=241 oxygen=false locked=true rings=false rotation=54488 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8396159_3733749_-457386 8396165_3733749_-457390 type=ice mass=0.16244683723102227 radius=0.6440401650263456 gravity=39 pressure=26 tempK=122 oxygen=false locked=true rings=false rotation=14579 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8396159_3733749_-457386 8396168_3733749_-457421 type=ice mass=0.8468118224661318 radius=0.9249787785253212 gravity=99 pressure=1600 tempK=123 oxygen=false locked=false rings=false rotation=33385 metallicity=1.3077954392164517 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 874922_4033773_3027920 874922_4033773_3027920 type=superearth mass=25.378726470010758 radius=2.2898780119292534 gravity=400 pressure=0 tempK=52 oxygen=false locked=false rings=false rotation=20865 metallicity=0.8292978022576905 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9096316_8238459_-4076773 type=ice mass=0.2572052366743317 radius=0.6877040920784976 gravity=54 pressure=968 tempK=81 oxygen=false locked=false rings=false rotation=26980 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9101053_8238297_-4074078 type=icegiant mass=180.65292154779496 radius=8.602364603527008 gravity=244 pressure=1600 tempK=239 oxygen=false locked=false rings=true rotation=6403 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9101075_8238268_-4074884 type=greenhouse mass=20.20506361182462 radius=2.26358459386985 gravity=394 pressure=1600 tempK=401 oxygen=false locked=false rings=false rotation=7816 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9101095_8238254_-4074575 type=gasgiant mass=184.81715571505165 radius=8.688024092877786 gravity=245 pressure=1600 tempK=355 oxygen=false locked=false rings=false rotation=5906 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9101152_8238265_-4074753 type=barren mass=0.21047510072471026 radius=0.6675340300021155 gravity=47 pressure=19 tempK=245 oxygen=false locked=false rings=false rotation=11999 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9101287_8238269_-4075013 type=superearth mass=10.816647345624249 radius=1.949315024881134 gravity=285 pressure=1600 tempK=800 oxygen=false locked=false rings=false rotation=90594 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9101293_8238273_-4074920 type=unclassified mass=6.3839884707604115 radius=1.7569735099238113 gravity=207 pressure=1 tempK=7936 oxygen=false locked=true rings=false rotation=16466 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9101309_8238211_-4076205 type=superearth mass=13.734120478770192 radius=2.055381652866782 gravity=325 pressure=1600 tempK=215 oxygen=false locked=false rings=false rotation=17909 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9101335_8238275_-4074965 type=desert mass=0.6863398424781832 radius=0.8648616554787374 gravity=92 pressure=34 tempK=436 oxygen=false locked=false rings=false rotation=16923 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9101349_8238185_-4077785 type=icegiant mass=98.5234480978337 radius=6.6090226899882865 gravity=226 pressure=1600 tempK=132 oxygen=false locked=false rings=true rotation=8317 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9101293_8238273_-4074920 9102690_8238598_-4083301 type=gasgiant mass=63.509722984799666 radius=5.460400114589725 gravity=213 pressure=1600 tempK=76 oxygen=false locked=false rings=false rotation=5180 metallicity=0.4248930031748539 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9115079_5410474_1795046 9115079_5410474_1795046 type=ice mass=1.3086926327482205 radius=1.0266176771058164 gravity=124 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=8011 metallicity=1.5865813077558517 terrain=TerrainOption[NATIVE genType=0 w=1] - system -147990_-4259982_-3782514 id=-1021692653 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless - system -1763249_4241372_4386755 id=-1507112769 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless - system -1811609_-4167359_4728900 id=-1319680873 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless - system -2114326_9425508_6200501 id=-525238425 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless - system -2550052_6994691_-1786506 id=-1940610717 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless - system -2559146_-594813_8660842 id=-322493161 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless - system -3234634_7878403_530638 id=-782417921 kind=ROGUE_PLANET name=PGR--5002361.5002361.0 starless - system -4729070_1810660_-2040913 id=-1026598645 kind=ROGUE_PLANET name=PGR--5002361.0.-5002361 starless - system -517007_1928357_7786828 id=-819120817 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless - system 2446288_9823217_1140272 id=-677694141 kind=STAR name=PGS-0.5002361.0 starTemp=40 starSize=0.6166995763778687 - system 331018_-2627546_4002550 id=-778284213 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless - system 3810943_7578176_-1529346 id=-1781882117 kind=STAR name=PGS-0.5002361.-5002361 starTemp=40 starSize=0.7512625455856323 - system 4285126_880860_5633403 id=-1076361445 kind=STAR name=PGS-0.0.5002361 starTemp=70 starSize=0.9343165159225464 - system 4287194_-1179392_-2651040 id=-493026021 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless - system 4540244_-2195284_6241886 id=-1311232065 kind=STAR name=PGS-0.-5002361.5002361 starTemp=70 starSize=0.9332678318023682 - system 548615_3409698_-890024 id=-502144617 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless - system 5659038_1332946_1424772 id=-1094984105 kind=STAR name=PGS-5002361.0.0 starTemp=70 starSize=0.9943618178367615 - system 6253955_-322304_1235104 id=-1379053845 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless - system 6326499_-4297175_-3049933 id=-335655149 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless - system 7518342_4618068_5122633 id=-1527090629 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless - system 783965_8560023_8900358 id=-602652429 kind=ROGUE_PLANET name=PGR-0.5002361.5002361 starless - system 8253893_-3146511_6379169 id=-1913306833 kind=STAR name=PGS-5002361.-5002361.5002361 starTemp=40 starSize=0.6768931150436401 - system 8259566_6483340_6516020 id=-373298525 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless - system 8396159_3733749_-457386 id=-1235930289 kind=STAR name=PGS-5002361.0.-5002361 starTemp=40 starSize=0.7446791529655457 - system 874922_4033773_3027920 id=-1958754413 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 9101293_8238273_-4074920 id=-1857245365 kind=STAR name=PGS-5002361.5002361.-5002361 starTemp=220 starSize=1.9884947538375854 - system 9115079_5410474_1795046 id=-1757034753 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless + body -1093770_2928517_-2936619 -1093710_2928517_-2936631 kind=MOON orbit=330 radius=0.2349662187411855 starId=-953287813 frame=false at=176948,0,-210510 + body -1093770_2928517_-2936619 -1093710_2928517_-2936631 kind=MOON orbit=330 radius=0.2736195914688149 starId=-953287813 frame=false at=-18793,0,-189671 + body -1093770_2928517_-2936619 -1093710_2928517_-2936631 kind=PLANET orbit=330 radius=1.0640082304214584 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093714_2928520_-2936554 kind=MOON orbit=458 radius=0.511620609578344 starId=-953287813 frame=false at=-79088,0,33268 + body -1093770_2928517_-2936619 -1093714_2928520_-2936554 kind=PLANET orbit=458 radius=0.7478926338807454 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093748_2928518_-2936619 kind=GAS_GIANT orbit=119 radius=10.193231645391162 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093750_2928505_-2936328 kind=PLANET orbit=1562 radius=0.20027460427854427 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093763_2928518_-2936631 kind=MOON orbit=73 radius=0.25298812765572387 starId=-953287813 frame=false at=133586,0,65998 + body -1093770_2928517_-2936619 -1093763_2928518_-2936631 kind=MOON orbit=73 radius=0.6787510506387318 starId=-953287813 frame=false at=280077,0,65349 + body -1093770_2928517_-2936619 -1093763_2928518_-2936631 kind=PLANET orbit=73 radius=1.1267386015753886 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093768_2928515_-2936470 kind=GAS_GIANT orbit=798 radius=10.529056608271485 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093768_2928515_-2936470 kind=MOON orbit=798 radius=0.2746479955738943 starId=-953287813 frame=false at=-1529796,0,-1576874 + body -1093770_2928517_-2936619 -1093769_2928517_-2936617 kind=STAR orbit=13 radius=69.80905731260776 starId=-953287814 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093770_2928517_-2936619 kind=STAR orbit=0 radius=0.0 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093771_2928517_-2936631 kind=PLANET orbit=62 radius=0.2851886889287729 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093778_2928517_-2936610 kind=ASTEROID_BELT orbit=66 radius=0.0 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093791_2928516_-2936641 kind=GAS_GIANT orbit=162 radius=3.3296796457692377 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093791_2928516_-2936641 kind=MOON orbit=162 radius=0.22354478862853186 starId=-953287813 frame=false at=491318,0,-871898 + body -1093770_2928517_-2936619 -1093791_2928516_-2936641 kind=MOON orbit=162 radius=0.26443852401660384 starId=-953287813 frame=false at=-704343,0,-287076 + body -1093770_2928517_-2936619 -1093811_2928515_-2936618 kind=MOON orbit=220 radius=0.5192411316972985 starId=-953287813 frame=false at=98567,0,67031 + body -1093770_2928517_-2936619 -1093811_2928515_-2936618 kind=MOON orbit=220 radius=0.7261851786087157 starId=-953287813 frame=false at=52964,0,91588 + body -1093770_2928517_-2936619 -1093811_2928515_-2936618 kind=PLANET orbit=220 radius=0.41868787916123795 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093828_2928521_-2936524 kind=GAS_GIANT orbit=597 radius=6.967877626193331 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1093967_2928517_-2936558 kind=GAS_GIANT orbit=1103 radius=5.229190104169739 starId=-953287813 frame=true at=0,0,0 + body -1093770_2928517_-2936619 -1094040_2928511_-2937000 kind=ASTEROID_BELT orbit=2499 radius=0.0 starId=-953287813 frame=true at=0,0,0 + body -1332546_1631083_3243750 -1332546_1631083_3243750 kind=MOON orbit=0 radius=0.4135480649211427 starId=-1436132233 frame=false at=41673,0,19000 + body -1332546_1631083_3243750 -1332546_1631083_3243750 kind=MOON orbit=0 radius=2.2855349913038356 starId=-1436132233 frame=false at=51650,0,-27681 + body -1332546_1631083_3243750 -1332546_1631083_3243750 kind=ROGUE_PLANET orbit=0 radius=0.20027007926536833 starId=-1436132233 frame=true at=0,0,0 + body -1502067_-1460437_4865055 -1502067_-1460437_4865055 kind=MOON orbit=0 radius=2.232997121416223 starId=-1542560749 frame=false at=-110869,0,-184209 + body -1502067_-1460437_4865055 -1502067_-1460437_4865055 kind=ROGUE_PLANET orbit=0 radius=0.8722168368675713 starId=-1542560749 frame=true at=0,0,0 + body -2327343_6217060_-3227882 -2327248_6217066_-3227948 kind=ASTEROID_BELT orbit=622 radius=0.0 starId=-638227073 frame=true at=0,0,0 + body -2327343_6217060_-3227882 -2327331_6217060_-3227869 kind=GAS_GIANT orbit=96 radius=6.206626820771696 starId=-638227073 frame=true at=0,0,0 + body -2327343_6217060_-3227882 -2327343_6217060_-3227882 kind=STAR orbit=0 radius=0.0 starId=-638227073 frame=true at=0,0,0 + body -2327343_6217060_-3227882 -2327344_6217060_-3227874 kind=PLANET orbit=41 radius=0.617305205704887 starId=-638227073 frame=true at=0,0,0 + body -2327343_6217060_-3227882 -2327344_6217060_-3227879 kind=PLANET orbit=15 radius=0.6433805032761719 starId=-638227073 frame=true at=0,0,0 + body -2327343_6217060_-3227882 -2327353_6217060_-3227880 kind=ASTEROID_BELT orbit=53 radius=0.0 starId=-638227073 frame=true at=0,0,0 + body -2327343_6217060_-3227882 -2327394_6217059_-3227934 kind=MOON orbit=389 radius=0.26035692394745363 starId=-638227073 frame=false at=78722,0,18208 + body -2327343_6217060_-3227882 -2327394_6217059_-3227934 kind=PLANET orbit=389 radius=0.263927013854739 starId=-638227073 frame=true at=0,0,0 + body -3264137_6862129_5633995 -3264137_6862129_5633995 kind=ROGUE_PLANET orbit=0 radius=1.1459793325492187 starId=-1878334477 frame=true at=0,0,0 + body -3272771_-2281626_-427326 -3272606_-2281621_-427320 kind=ASTEROID_BELT orbit=884 radius=0.0 starId=-1787505529 frame=true at=0,0,0 + body -3272771_-2281626_-427326 -3272685_-2281627_-427268 kind=GAS_GIANT orbit=553 radius=4.924089526202804 starId=-1787505529 frame=true at=0,0,0 + body -3272771_-2281626_-427326 -3272728_-2281624_-427344 kind=PLANET orbit=248 radius=0.48115646573370274 starId=-1787505529 frame=true at=0,0,0 + body -3272771_-2281626_-427326 -3272756_-2281626_-427334 kind=PLANET orbit=89 radius=1.6998160251693322 starId=-1787505529 frame=true at=0,0,0 + body -3272771_-2281626_-427326 -3272770_-2281626_-427322 kind=PLANET orbit=24 radius=1.1261802381726516 starId=-1787505529 frame=true at=0,0,0 + body -3272771_-2281626_-427326 -3272770_-2281626_-427327 kind=PLANET orbit=7 radius=1.0766820549511433 starId=-1787505529 frame=true at=0,0,0 + body -3272771_-2281626_-427326 -3272771_-2281626_-427326 kind=STAR orbit=0 radius=0.0 starId=-1787505529 frame=true at=0,0,0 + body -3272771_-2281626_-427326 -3272775_-2281626_-427332 kind=PLANET orbit=37 radius=0.4006648591407689 starId=-1787505529 frame=true at=0,0,0 + body -3272771_-2281626_-427326 -3272786_-2281624_-427271 kind=ASTEROID_BELT orbit=307 radius=0.0 starId=-1787505529 frame=true at=0,0,0 + body -3382269_4609397_102289 -3382162_4609391_102429 kind=ASTEROID_BELT orbit=945 radius=0.0 starId=-1804580365 frame=true at=0,0,0 + body -3382269_4609397_102289 -3382258_4609397_102291 kind=PLANET orbit=62 radius=1.1945742911747057 starId=-1804580365 frame=true at=0,0,0 + body -3382269_4609397_102289 -3382269_4609397_102289 kind=STAR orbit=0 radius=0.0 starId=-1804580365 frame=true at=0,0,0 + body -3382269_4609397_102289 -3382271_4609397_102293 kind=PLANET orbit=24 radius=2.4553590705256463 starId=-1804580365 frame=true at=0,0,0 + body -3382269_4609397_102289 -3382275_4609398_102253 kind=PLANET orbit=193 radius=0.32843835951638584 starId=-1804580365 frame=true at=0,0,0 + body -3382269_4609397_102289 -3382347_4609393_102367 kind=MOON orbit=591 radius=0.22586605779187946 starId=-1804580365 frame=false at=-414275,0,-295043 + body -3382269_4609397_102289 -3382347_4609393_102367 kind=MOON orbit=591 radius=0.23037156673038364 starId=-1804580365 frame=false at=122866,0,-24063 + body -3382269_4609397_102289 -3382347_4609393_102367 kind=PLANET orbit=591 radius=1.7294190750209564 starId=-1804580365 frame=true at=0,0,0 + body -3382269_4609397_102289 -3382624_4609397_102449 kind=STAR orbit=2080 radius=72.31004428625107 starId=-1804580366 frame=true at=0,0,0 + body -446119_-2387600_1528743 -430224_-2387600_1538055 kind=STAR orbit=98513 radius=70.60047593951225 starId=-1238572758 frame=true at=0,0,0 + body -446119_-2387600_1528743 -446071_-2387599_1528765 kind=MOON orbit=285 radius=0.24309845196571753 starId=-1238572757 frame=false at=296699,0,-7759 + body -446119_-2387600_1528743 -446071_-2387599_1528765 kind=PLANET orbit=285 radius=1.8371459761843838 starId=-1238572757 frame=true at=0,0,0 + body -446119_-2387600_1528743 -446073_-2387603_1528672 kind=ASTEROID_BELT orbit=456 radius=0.0 starId=-1238572757 frame=true at=0,0,0 + body -446119_-2387600_1528743 -446114_-2387600_1528734 kind=GAS_GIANT orbit=53 radius=7.324245297594027 starId=-1238572757 frame=true at=0,0,0 + body -446119_-2387600_1528743 -446114_-2387600_1528734 kind=MOON orbit=53 radius=0.2315350492962238 starId=-1238572757 frame=false at=706219,0,-929558 + body -446119_-2387600_1528743 -446114_-2387600_1528734 kind=MOON orbit=53 radius=0.38143339345610094 starId=-1238572757 frame=false at=-1782708,0,1014985 + body -446119_-2387600_1528743 -446114_-2387600_1528734 kind=MOON orbit=53 radius=0.5248470677369199 starId=-1238572757 frame=false at=126197,0,489391 + body -446119_-2387600_1528743 -446116_-2387600_1528747 kind=ASTEROID_BELT orbit=29 radius=0.0 starId=-1238572757 frame=true at=0,0,0 + body -446119_-2387600_1528743 -446118_-2387600_1528741 kind=MOON orbit=13 radius=0.20365004413625312 starId=-1238572757 frame=false at=-72832,0,-71410 + body -446119_-2387600_1528743 -446118_-2387600_1528741 kind=PLANET orbit=13 radius=1.3953997874664419 starId=-1238572757 frame=true at=0,0,0 + body -446119_-2387600_1528743 -446119_-2387600_1528743 kind=STAR orbit=0 radius=0.0 starId=-1238572757 frame=true at=0,0,0 + body -612264_1154834_5810641 -612264_1154834_5810641 kind=ROGUE_PLANET orbit=0 radius=0.4898994165169739 starId=-912475673 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1301912_-3086588_1331515 kind=ASTEROID_BELT orbit=3240 radius=0.0 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302043_-3086592_1332186 kind=GAS_GIANT orbit=662 radius=10.943876624718687 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302078_-3086584_1332016 kind=GAS_GIANT orbit=429 radius=3.7203469240327802 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302078_-3086584_1332016 kind=MOON orbit=429 radius=0.20017428524080313 starId=-993759433 frame=false at=-553114,0,-371691 + body 1302116_-3086586_1332086 1302078_-3086584_1332016 kind=MOON orbit=429 radius=0.38607883059392345 starId=-993759433 frame=false at=861833,0,-444261 + body 1302116_-3086586_1332086 1302107_-3086586_1332065 kind=GAS_GIANT orbit=121 radius=10.911882841329781 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302109_-3086586_1332079 kind=PLANET orbit=53 radius=0.20168145982592578 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302113_-3086586_1332071 kind=PLANET orbit=81 radius=0.3769303000187614 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302114_-3086586_1332089 kind=MOON orbit=20 radius=0.23277704186615006 starId=-993759433 frame=false at=512423,0,328354 + body 1302116_-3086586_1332086 1302114_-3086586_1332089 kind=MOON orbit=20 radius=0.4590817541896891 starId=-993759433 frame=false at=-59243,0,-288173 + body 1302116_-3086586_1332086 1302114_-3086586_1332089 kind=PLANET orbit=20 radius=2.0413133728416963 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302116_-3086586_1332086 kind=STAR orbit=0 radius=0.0 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302118_-3086586_1332087 kind=MOON orbit=13 radius=0.25425560347671766 starId=-993759433 frame=false at=50790,0,16491 + body 1302116_-3086586_1332086 1302118_-3086586_1332087 kind=MOON orbit=13 radius=0.3763175993207074 starId=-993759433 frame=false at=27886,0,-85992 + body 1302116_-3086586_1332086 1302118_-3086586_1332087 kind=PLANET orbit=13 radius=0.3079462488366804 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302121_-3086586_1332088 kind=PLANET orbit=29 radius=0.6327276283379721 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302129_-3086585_1332086 kind=ASTEROID_BELT orbit=67 radius=0.0 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302140_-3086586_1332065 kind=PLANET orbit=169 radius=2.3337921361694023 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302170_-3086588_1332078 kind=MOON orbit=294 radius=0.20052505369228113 starId=-993759433 frame=false at=-36866,0,7392 + body 1302116_-3086586_1332086 1302170_-3086588_1332078 kind=PLANET orbit=294 radius=0.36453462415151583 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302184_-3086594_1331905 kind=PLANET orbit=1032 radius=0.6157141695354549 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302488_-3086594_1332155 kind=GAS_GIANT orbit=2025 radius=10.002498125514519 starId=-993759433 frame=true at=0,0,0 + body 1302116_-3086586_1332086 1302488_-3086594_1332155 kind=MOON orbit=2025 radius=0.4638800680553467 starId=-993759433 frame=false at=806665,0,655155 + body 1302116_-3086586_1332086 1302488_-3086594_1332155 kind=MOON orbit=2025 radius=0.5150449302607414 starId=-993759433 frame=false at=-872558,0,997395 + body 1609421_-1918473_4755340 1609421_-1918473_4755340 kind=ROGUE_PLANET orbit=0 radius=0.7827230613996494 starId=-57202221 frame=true at=0,0,0 + body 1899532_3100584_4509498 1899470_3100581_4509619 kind=ASTEROID_BELT orbit=729 radius=0.0 starId=-810501533 frame=true at=0,0,0 + body 1899532_3100584_4509498 1899514_3100584_4509415 kind=PLANET orbit=456 radius=0.20516492708996092 starId=-810501533 frame=true at=0,0,0 + body 1899532_3100584_4509498 1899521_3100583_4509481 kind=MOON orbit=109 radius=0.3670423958318636 starId=-810501533 frame=false at=64062,0,-34158 + body 1899532_3100584_4509498 1899521_3100583_4509481 kind=PLANET orbit=109 radius=0.32640763764967407 starId=-810501533 frame=true at=0,0,0 + body 1899532_3100584_4509498 1899531_3100584_4509493 kind=PLANET orbit=25 radius=0.8240496319587478 starId=-810501533 frame=true at=0,0,0 + body 1899532_3100584_4509498 1899532_3100584_4509497 kind=PLANET orbit=7 radius=1.278950213656414 starId=-810501533 frame=true at=0,0,0 + body 1899532_3100584_4509498 1899532_3100584_4509498 kind=STAR orbit=0 radius=0.0 starId=-810501533 frame=true at=0,0,0 + body 2031833_5218371_1147521 2031833_5218371_1147521 kind=MOON orbit=0 radius=1.5534761570116726 starId=-770357401 frame=false at=-36421,0,249152 + body 2031833_5218371_1147521 2031833_5218371_1147521 kind=ROGUE_PLANET orbit=0 radius=1.2501648106704106 starId=-770357401 frame=true at=0,0,0 + body 2106667_5115784_6590371 2106667_5115784_6590371 kind=MOON orbit=0 radius=1.666309767693942 starId=-714236625 frame=false at=-226712,0,242266 + body 2106667_5115784_6590371 2106667_5115784_6590371 kind=ROGUE_PLANET orbit=0 radius=1.4995482876777844 starId=-714236625 frame=true at=0,0,0 + body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=GAS_GIANT orbit=21 radius=9.510681930029026 starId=-196901933 frame=true at=0,0,0 + body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=MOON orbit=21 radius=0.2091317529424837 starId=-196901933 frame=false at=-867790,0,-837770 + body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=MOON orbit=21 radius=0.33631052457814614 starId=-196901933 frame=false at=557220,0,-1629987 + body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=MOON orbit=21 radius=0.4688220383655022 starId=-196901933 frame=false at=-1860655,0,750736 + body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=MOON orbit=21 radius=0.6909717843738756 starId=-196901933 frame=false at=1857976,0,-1173240 + body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=GAS_GIANT orbit=220 radius=8.311637957512806 starId=-196901933 frame=true at=0,0,0 + body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.21016561089341101 starId=-196901933 frame=false at=-1863525,0,-132088 + body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.30216191398938774 starId=-196901933 frame=false at=-166931,0,-2046202 + body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.565144757070652 starId=-196901933 frame=false at=814570,0,1081569 + body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.5890525604324046 starId=-196901933 frame=false at=1690816,0,488611 + body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.6746556547268353 starId=-196901933 frame=false at=1024779,0,768961 + body 2553003_-535245_-1050940 2553002_-535245_-1050920 kind=PLANET orbit=105 radius=2.296973618840463 starId=-196901933 frame=true at=0,0,0 + body 2553003_-535245_-1050940 2553003_-535245_-1050940 kind=STAR orbit=0 radius=0.0 starId=-196901933 frame=true at=0,0,0 + body 2553003_-535245_-1050940 2553004_-535245_-1050942 kind=PLANET orbit=10 radius=0.6766914215527544 starId=-196901933 frame=true at=0,0,0 + body 2553003_-535245_-1050940 2553005_-535245_-1050941 kind=ASTEROID_BELT orbit=11 radius=0.0 starId=-196901933 frame=true at=0,0,0 + body 2553003_-535245_-1050940 2553009_-535245_-1050938 kind=PLANET orbit=34 radius=0.5304012228503437 starId=-196901933 frame=true at=0,0,0 + body 2553003_-535245_-1050940 2553035_-535241_-1051068 kind=ASTEROID_BELT orbit=704 radius=0.0 starId=-196901933 frame=true at=0,0,0 + body 2553003_-535245_-1050940 2553062_-535246_-1050997 kind=PLANET orbit=440 radius=0.3249400421494298 starId=-196901933 frame=true at=0,0,0 + body 3411017_1872378_2686670 3411017_1872378_2686670 kind=MOON orbit=0 radius=0.3524650663904568 starId=-1958754413 frame=false at=376684,0,-136342 + body 3411017_1872378_2686670 3411017_1872378_2686670 kind=MOON orbit=0 radius=1.4496425342156365 starId=-1958754413 frame=false at=271102,0,-165833 + body 3411017_1872378_2686670 3411017_1872378_2686670 kind=ROGUE_PLANET orbit=0 radius=1.603840597316356 starId=-1958754413 frame=true at=0,0,0 + body 3631783_-3208848_-3037694 3631783_-3208848_-3037694 kind=ROGUE_PLANET orbit=0 radius=1.152239748699427 starId=-43961145 frame=true at=0,0,0 + body 395746_2934615_-3374968 395746_2934615_-3374968 kind=MOON orbit=0 radius=1.3734148264964914 starId=-1652144877 frame=false at=20980,0,6623 + body 395746_2934615_-3374968 395746_2934615_-3374968 kind=ROGUE_PLANET orbit=0 radius=0.22103981078535706 starId=-1652144877 frame=true at=0,0,0 + body 4348163_4426685_2048679 4348163_4426685_2048679 kind=MOON orbit=0 radius=0.22387906715845088 starId=-1258055601 frame=false at=52859,0,101681 + body 4348163_4426685_2048679 4348163_4426685_2048679 kind=MOON orbit=0 radius=1.9102745823110474 starId=-1258055601 frame=false at=-17435,0,-285067 + body 4348163_4426685_2048679 4348163_4426685_2048679 kind=ROGUE_PLANET orbit=0 radius=0.9424057339265934 starId=-1258055601 frame=true at=0,0,0 + body 4836351_4419696_6894907 4836311_4419696_6894896 kind=PLANET orbit=222 radius=1.544147080458167 starId=-1565633077 frame=true at=0,0,0 + body 4836351_4419696_6894907 4836340_4419696_6894906 kind=GAS_GIANT orbit=57 radius=10.24002265252395 starId=-1565633077 frame=true at=0,0,0 + body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.20147518559961805 starId=-1565633077 frame=false at=581025,0,467576 + body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.45081664972260455 starId=-1565633077 frame=false at=878658,0,-554884 + body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.46322695463417846 starId=-1565633077 frame=false at=-2464488,0,-1133356 + body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.6489701106320351 starId=-1565633077 frame=false at=-1191490,0,-1549204 + body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.7380916191408169 starId=-1565633077 frame=false at=916270,0,-264200 + body 4836351_4419696_6894907 4836350_4419696_6894904 kind=ASTEROID_BELT orbit=15 radius=0.0 starId=-1565633077 frame=true at=0,0,0 + body 4836351_4419696_6894907 4836351_4419696_6894902 kind=GAS_GIANT orbit=27 radius=3.402256404961512 starId=-1565633077 frame=true at=0,0,0 + body 4836351_4419696_6894907 4836351_4419696_6894907 kind=STAR orbit=0 radius=0.0 starId=-1565633077 frame=true at=0,0,0 + body 4836351_4419696_6894907 4836351_4419696_6894909 kind=PLANET orbit=9 radius=0.20845924847231542 starId=-1565633077 frame=true at=0,0,0 + body 4836351_4419696_6894907 4836371_4419695_6894919 kind=PLANET orbit=126 radius=0.3528799763588127 starId=-1565633077 frame=true at=0,0,0 + body 4836351_4419696_6894907 4836414_4419699_6894927 kind=ASTEROID_BELT orbit=355 radius=0.0 starId=-1565633077 frame=true at=0,0,0 + body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=GAS_GIANT orbit=257 radius=10.283996064694179 starId=-893135689 frame=true at=0,0,0 + body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.20127083174628194 starId=-893135689 frame=false at=-432171,0,2295879 + body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.3764121020262485 starId=-893135689 frame=false at=-582054,0,-671434 + body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.40473385249339855 starId=-893135689 frame=false at=461445,0,-664736 + body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.7009865591178472 starId=-893135689 frame=false at=-949926,0,-1103439 + body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.7359933058039947 starId=-893135689 frame=false at=1019184,0,925955 + body 5393152_-2277199_2439904 5393136_-2277200_2439909 kind=GAS_GIANT orbit=89 radius=6.858044787609019 starId=-893135689 frame=true at=0,0,0 + body 5393152_-2277199_2439904 5393136_-2277200_2439909 kind=MOON orbit=89 radius=0.3398250696919669 starId=-893135689 frame=false at=-419343,0,580351 + body 5393152_-2277199_2439904 5393136_-2277200_2439909 kind=MOON orbit=89 radius=0.3467426497803343 starId=-893135689 frame=false at=-335138,0,-285718 + body 5393152_-2277199_2439904 5393136_-2277200_2439909 kind=MOON orbit=89 radius=0.7406934049445608 starId=-893135689 frame=false at=-1806393,0,-681860 + body 5393152_-2277199_2439904 5393151_-2277199_2439895 kind=ASTEROID_BELT orbit=49 radius=0.0 starId=-893135689 frame=true at=0,0,0 + body 5393152_-2277199_2439904 5393152_-2277199_2439904 kind=STAR orbit=0 radius=0.0 starId=-893135689 frame=true at=0,0,0 + body 5393152_-2277199_2439904 5393153_-2277199_2439905 kind=PLANET orbit=9 radius=1.2469482547976387 starId=-893135689 frame=true at=0,0,0 + body 5393152_-2277199_2439904 5393160_-2277199_2439900 kind=MOON orbit=46 radius=0.20402162240276098 starId=-893135689 frame=false at=-112747,0,-239156 + body 5393152_-2277199_2439904 5393160_-2277199_2439900 kind=MOON orbit=46 radius=0.47671909908637755 starId=-893135689 frame=false at=210052,0,146336 + body 5393152_-2277199_2439904 5393160_-2277199_2439900 kind=PLANET orbit=46 radius=0.9514122875049971 starId=-893135689 frame=true at=0,0,0 + body 5393152_-2277199_2439904 5393200_-2277200_2439964 kind=ASTEROID_BELT orbit=411 radius=0.0 starId=-893135689 frame=true at=0,0,0 + body 5854133_275080_2604160 5854133_275080_2604160 kind=MOON orbit=0 radius=2.238060855684214 starId=-1515893089 frame=false at=106472,0,-351216 + body 5854133_275080_2604160 5854133_275080_2604160 kind=ROGUE_PLANET orbit=0 radius=1.400785633512131 starId=-1515893089 frame=true at=0,0,0 + body 5891934_-757386_5055698 5891934_-757386_5055698 kind=MOON orbit=0 radius=1.308200595402922 starId=-1906018109 frame=false at=-99690,0,-221171 + body 5891934_-757386_5055698 5891934_-757386_5055698 kind=ROGUE_PLANET orbit=0 radius=1.893272712804346 starId=-1906018109 frame=true at=0,0,0 + body 6386779_6218703_-2290675 6386779_6218703_-2290675 kind=MOON orbit=0 radius=0.2600017031861839 starId=-788062221 frame=false at=-202560,0,34037 + body 6386779_6218703_-2290675 6386779_6218703_-2290675 kind=MOON orbit=0 radius=2.430948757116413 starId=-788062221 frame=false at=52230,0,-149753 + body 6386779_6218703_-2290675 6386779_6218703_-2290675 kind=ROGUE_PLANET orbit=0 radius=1.1355178221934288 starId=-788062221 frame=true at=0,0,0 + body 6918713_2294940_-820156 6918708_2294940_-820150 kind=MOON orbit=43 radius=0.324471229410524 starId=-1120847449 frame=false at=221671,0,413988 + body 6918713_2294940_-820156 6918708_2294940_-820150 kind=MOON orbit=43 radius=0.41469569651648625 starId=-1120847449 frame=false at=-232412,0,-19159 + body 6918713_2294940_-820156 6918708_2294940_-820150 kind=PLANET orbit=43 radius=1.6772381770058735 starId=-1120847449 frame=true at=0,0,0 + body 6918713_2294940_-820156 6918712_2294940_-820152 kind=MOON orbit=22 radius=0.22253218956390367 starId=-1120847449 frame=false at=21983,0,13884 + body 6918713_2294940_-820156 6918712_2294940_-820152 kind=MOON orbit=22 radius=0.2298457183025389 starId=-1120847449 frame=false at=-24956,0,61748 + body 6918713_2294940_-820156 6918712_2294940_-820152 kind=PLANET orbit=22 radius=0.2625801459682835 starId=-1120847449 frame=true at=0,0,0 + body 6918713_2294940_-820156 6918713_2294940_-820156 kind=STAR orbit=0 radius=0.0 starId=-1120847449 frame=true at=0,0,0 + body 6918713_2294940_-820156 6918715_2294941_-820138 kind=MOON orbit=96 radius=0.5580329812757592 starId=-1120847449 frame=false at=-195270,0,55484 + body 6918713_2294940_-820156 6918715_2294941_-820138 kind=PLANET orbit=96 radius=2.26108180661609 starId=-1120847449 frame=true at=0,0,0 + body 6918713_2294940_-820156 6918743_2294939_-820129 kind=PLANET orbit=217 radius=1.8296530727395168 starId=-1120847449 frame=true at=0,0,0 + body 6918713_2294940_-820156 6918773_2294940_-820077 kind=PLANET orbit=529 radius=1.5512577339702143 starId=-1120847449 frame=true at=0,0,0 + body 6918713_2294940_-820156 6918912_2294932_-820033 kind=MOON orbit=1251 radius=0.42418680649418383 starId=-1120847449 frame=false at=180222,0,418439 + body 6918713_2294940_-820156 6918912_2294932_-820033 kind=PLANET orbit=1251 radius=1.5135313072630292 starId=-1120847449 frame=true at=0,0,0 + body 6918713_2294940_-820156 6919013_2294959_-820378 kind=ASTEROID_BELT orbit=2001 radius=0.0 starId=-1120847449 frame=true at=0,0,0 + body 6947973_1979802_5101896 6947973_1979802_5101896 kind=MOON orbit=0 radius=0.9725300949324311 starId=-1165534741 frame=false at=-33882,0,-362621 + body 6947973_1979802_5101896 6947973_1979802_5101896 kind=ROGUE_PLANET orbit=0 radius=1.4038279741834425 starId=-1165534741 frame=true at=0,0,0 + body 878562_5596671_-502705 877083_5596637_-502177 kind=ASTEROID_BELT orbit=8398 radius=0.0 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878506_5596691_-502296 kind=GAS_GIANT orbit=2210 radius=8.44842360519568 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878506_5596691_-502296 kind=MOON orbit=2210 radius=0.20663708684964743 starId=-1528641933 frame=false at=-507604,0,-1664731 + body 878562_5596671_-502705 878506_5596691_-502296 kind=MOON orbit=2210 radius=0.35080038054064144 starId=-1528641933 frame=false at=1869447,0,-1303810 + body 878562_5596671_-502705 878516_5596673_-502729 kind=GAS_GIANT orbit=279 radius=7.655802025985872 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878534_5596672_-502627 kind=PLANET orbit=442 radius=0.8168679635028027 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878553_5596671_-502711 kind=PLANET orbit=56 radius=1.0779650352367258 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878554_5596670_-502677 kind=ASTEROID_BELT orbit=155 radius=0.0 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878556_5596671_-502707 kind=PLANET orbit=35 radius=0.7163408393411195 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878558_5596671_-502734 kind=MOON orbit=155 radius=0.21301645293632387 starId=-1528641933 frame=false at=-27422,0,-63535 + body 878562_5596671_-502705 878558_5596671_-502734 kind=MOON orbit=155 radius=0.6471535720830919 starId=-1528641933 frame=false at=18589,0,47921 + body 878562_5596671_-502705 878558_5596671_-502734 kind=PLANET orbit=155 radius=0.581475535515763 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878559_5596671_-502685 kind=PLANET orbit=109 radius=1.041678706292689 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878562_5596671_-502705 kind=STAR orbit=0 radius=0.0 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878620_5596668_-502556 kind=PLANET orbit=857 radius=0.2455048607815313 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878732_5596624_-501739 kind=GAS_GIANT orbit=5249 radius=6.895570710616032 starId=-1528641933 frame=true at=0,0,0 + body 878562_5596671_-502705 878732_5596624_-501739 kind=MOON orbit=5249 radius=0.5214014902948397 starId=-1528641933 frame=false at=-381512,0,-728102 + body 878562_5596671_-502705 878732_5596624_-501739 kind=MOON orbit=5249 radius=0.5402142647908047 starId=-1528641933 frame=false at=-1068581,0,-731192 + body 878562_5596671_-502705 878777_5596663_-502864 kind=PLANET orbit=1431 radius=1.9146860220043695 starId=-1528641933 frame=true at=0,0,0 + derived -1093770_2928517_-2936619 -1093710_2928517_-2936631 type=ice mass=1.3964456469001738 radius=1.0640082304214584 gravity=123 pressure=1600 tempK=186 oxygen=false locked=false rings=false rotation=89617 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093714_2928520_-2936554 type=ice mass=0.32402193776600785 radius=0.7478926338807454 gravity=58 pressure=481 tempK=117 oxygen=false locked=false rings=false rotation=46328 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093748_2928518_-2936619 type=gasgiant mass=266.8955298371472 radius=10.193231645391162 gravity=257 pressure=1600 tempK=327 oxygen=false locked=false rings=false rotation=13026 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093750_2928505_-2936328 type=barren mass=0.0020613091983445507 radius=0.20027460427854427 gravity=5 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=16236 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093763_2928518_-2936631 type=greenhouse mass=1.8371551952464107 radius=1.1267386015753886 gravity=145 pressure=1519 tempK=347 oxygen=false locked=false rings=false rotation=9612 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093768_2928515_-2936470 type=gasgiant mass=287.5541922555389 radius=10.529056608271485 gravity=259 pressure=1600 tempK=126 oxygen=false locked=false rings=true rotation=10936 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093769_2928517_-2936617 type=barren mass=0.0031935937678598536 radius=0.20075632583676514 gravity=8 pressure=0 tempK=504 oxygen=false locked=true rings=false rotation=16072 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093770_2928517_-2936619 type=lava mass=0.30001901663593344 radius=0.7063537592628808 gravity=60 pressure=0 tempK=1817 oxygen=false locked=true rings=false rotation=31735 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093771_2928517_-2936631 type=desert mass=0.01092556873493221 radius=0.2851886889287729 gravity=13 pressure=0 tempK=219 oxygen=false locked=false rings=false rotation=12926 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093778_2928517_-2936610 type=barren mass=0.002478191150655063 radius=0.20067793516870133 gravity=6 pressure=0 tempK=225 oxygen=false locked=false rings=false rotation=8229 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093791_2928516_-2936641 type=gasgiant mass=20.358678404345994 radius=3.3296796457692377 gravity=184 pressure=1600 tempK=280 oxygen=false locked=false rings=true rotation=4853 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093811_2928515_-2936618 type=ice mass=0.03961721912249209 radius=0.41868787916123795 gravity=23 pressure=4 tempK=101 oxygen=false locked=false rings=false rotation=75676 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093828_2928521_-2936524 type=gasgiant mass=111.26411759923079 radius=6.967877626193331 gravity=229 pressure=1600 tempK=146 oxygen=false locked=false rings=true rotation=6402 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1093967_2928517_-2936558 type=icegiant mass=57.494080748624015 radius=5.229190104169739 gravity=210 pressure=1600 tempK=107 oxygen=false locked=false rings=true rotation=13505 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1093770_2928517_-2936619 -1094040_2928511_-2937000 type=superearth mass=14.189075810777892 radius=2.110920648460378 gravity=318 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=56348 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1332546_1631083_3243750 -1332546_1631083_3243750 type=barren mass=0.0030360464590692155 radius=0.20027007926536833 gravity=8 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=43387 metallicity=1.2805395851689565 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1502067_-1460437_4865055 -1502067_-1460437_4865055 type=barren mass=0.5474176307060243 radius=0.8722168368675713 gravity=72 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=48570 metallicity=1.377567820292009 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2327343_6217060_-3227882 -2327248_6217066_-3227948 type=superearth mass=5.382876056475847 radius=1.6695391816792997 gravity=193 pressure=1600 tempK=92 oxygen=false locked=false rings=false rotation=32615 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2327343_6217060_-3227882 -2327331_6217060_-3227869 type=gasgiant mass=85.26914373948553 radius=6.206626820771696 gravity=221 pressure=1600 tempK=215 oxygen=false locked=false rings=true rotation=6630 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2327343_6217060_-3227882 -2327343_6217060_-3227882 type=lava mass=2.4503154630596082 radius=1.267855119066724 gravity=152 pressure=30 tempK=1089 oxygen=false locked=true rings=false rotation=71827 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2327343_6217060_-3227882 -2327344_6217060_-3227874 type=barren mass=0.17022683762316365 radius=0.617305205704887 gravity=45 pressure=19 tempK=169 oxygen=false locked=true rings=false rotation=37052 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2327343_6217060_-3227882 -2327344_6217060_-3227879 type=desert mass=0.19760497225790083 radius=0.6433805032761719 gravity=48 pressure=7 tempK=264 oxygen=false locked=true rings=false rotation=36780 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2327343_6217060_-3227882 -2327353_6217060_-3227880 type=superearth mass=5.647858716195294 radius=1.580146346818058 gravity=226 pressure=1600 tempK=316 oxygen=false locked=false rings=false rotation=69550 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2327343_6217060_-3227882 -2327394_6217059_-3227934 type=barren mass=0.006064086898440579 radius=0.263927013854739 gravity=9 pressure=0 tempK=54 oxygen=false locked=false rings=false rotation=29496 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3264137_6862129_5633995 -3264137_6862129_5633995 type=ice mass=1.9774137805355403 radius=1.1459793325492187 gravity=151 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=21373 metallicity=0.6222741151459 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3272771_-2281626_-427326 -3272606_-2281621_-427320 type=superearth mass=4.071980468622733 radius=1.38557474240371 gravity=212 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=22898 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3272771_-2281626_-427326 -3272685_-2281627_-427268 type=icegiant mass=50.06954063214108 radius=4.924089526202804 gravity=207 pressure=1600 tempK=87 oxygen=false locked=false rings=true rotation=5152 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3272771_-2281626_-427326 -3272728_-2281624_-427344 type=ice mass=0.07395833217578861 radius=0.48115646573370274 gravity=32 pressure=72 tempK=56 oxygen=false locked=false rings=false rotation=12791 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3272771_-2281626_-427326 -3272756_-2281626_-427334 type=superearth mass=8.066179180661075 radius=1.6998160251693322 gravity=279 pressure=1600 tempK=237 oxygen=false locked=false rings=false rotation=66257 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3272771_-2281626_-427326 -3272770_-2281626_-427322 type=ocean mass=1.1994237875618499 radius=1.1261802381726516 gravity=95 pressure=276 tempK=313 oxygen=false locked=true rings=true rotation=82311 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3272771_-2281626_-427326 -3272770_-2281626_-427327 type=desert mass=1.0209010517542583 radius=1.0766820549511433 gravity=88 pressure=40 tempK=375 oxygen=false locked=true rings=true rotation=80510 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3272771_-2281626_-427326 -3272771_-2281626_-427326 type=barren mass=0.0034226765713115154 radius=0.21269833424610543 gravity=8 pressure=0 tempK=1052 oxygen=false locked=true rings=false rotation=72845 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3272771_-2281626_-427326 -3272775_-2281626_-427332 type=barren mass=0.029519389376990515 radius=0.4006648591407689 gravity=18 pressure=1 tempK=173 oxygen=false locked=true rings=false rotation=39886 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3272771_-2281626_-427326 -3272786_-2281624_-427271 type=ice mass=0.009627940168547167 radius=0.2734110634313652 gravity=13 pressure=1 tempK=49 oxygen=false locked=false rings=false rotation=21230 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3382269_4609397_102289 -3382162_4609391_102429 type=ice mass=0.08994449968247054 radius=0.5110071166882669 gravity=34 pressure=9 tempK=71 oxygen=false locked=false rings=false rotation=38907 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3382269_4609397_102289 -3382258_4609397_102291 type=greenhouse mass=2.0405581126019174 radius=1.1945742911747057 gravity=143 pressure=291 tempK=364 oxygen=false locked=false rings=false rotation=29026 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3382269_4609397_102289 -3382269_4609397_102289 type=lava mass=11.511494176857271 radius=2.040002113160309 gravity=277 pressure=62 tempK=2691 oxygen=false locked=true rings=false rotation=16716 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3382269_4609397_102289 -3382271_4609397_102293 type=greenhouse mass=22.281808900232875 radius=2.4553590705256463 gravity=370 pressure=1600 tempK=897 oxygen=false locked=true rings=false rotation=12918 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3382269_4609397_102289 -3382275_4609398_102253 type=ice mass=0.018314709335077232 radius=0.32843835951638584 gravity=17 pressure=0 tempK=158 oxygen=false locked=false rings=false rotation=21956 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3382269_4609397_102289 -3382347_4609393_102367 type=superearth mass=6.367156770825919 radius=1.7294190750209564 gravity=213 pressure=1600 tempK=233 oxygen=false locked=false rings=false rotation=6198 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3382269_4609397_102289 -3382624_4609397_102449 type=ice mass=0.7354859432149627 radius=0.8834632249979295 gravity=94 pressure=1600 tempK=108 oxygen=false locked=false rings=false rotation=43341 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -446119_-2387600_1528743 -430224_-2387600_1538055 type=ice mass=1.518364842304439 radius=1.0738446824738717 gravity=132 pressure=1600 tempK=5 oxygen=false locked=false rings=false rotation=19508 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -446119_-2387600_1528743 -446071_-2387599_1528765 type=ice mass=9.144461672947815 radius=1.8371459761843838 gravity=271 pressure=1600 tempK=96 oxygen=false locked=false rings=false rotation=32130 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -446119_-2387600_1528743 -446073_-2387603_1528672 type=icegiant mass=82.86029378521921 radius=6.1297755980796795 gravity=221 pressure=1600 tempK=80 oxygen=false locked=false rings=true rotation=11189 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -446119_-2387600_1528743 -446114_-2387600_1528734 type=icegiant mass=124.78965102386721 radius=7.324245297594027 gravity=233 pressure=1600 tempK=236 oxygen=false locked=false rings=true rotation=12164 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -446119_-2387600_1528743 -446116_-2387600_1528747 type=barren mass=0.12386907945589916 radius=0.5507257290050611 gravity=41 pressure=21 tempK=163 oxygen=false locked=true rings=false rotation=19690 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -446119_-2387600_1528743 -446118_-2387600_1528741 type=superearth mass=4.258372902034484 radius=1.3953997874664419 gravity=219 pressure=1600 tempK=520 oxygen=false locked=true rings=false rotation=39283 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -446119_-2387600_1528743 -446119_-2387600_1528743 type=barren mass=0.20274132430322034 radius=0.6666674967803836 gravity=46 pressure=0 tempK=882 oxygen=false locked=true rings=false rotation=6752 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -612264_1154834_5810641 -612264_1154834_5810641 type=ice mass=0.056942950901811534 radius=0.4898994165169739 gravity=24 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=95666 metallicity=1.1827308301341946 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1301912_-3086588_1331515 type=ice mass=6.727556099331668 radius=1.6761155997081671 gravity=239 pressure=1600 tempK=62 oxygen=false locked=false rings=false rotation=13868 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302043_-3086592_1332186 type=icegiant mass=314.28067515728264 radius=10.943876624718687 gravity=262 pressure=1600 tempK=147 oxygen=false locked=false rings=true rotation=10957 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302078_-3086584_1332016 type=gasgiant mass=26.276399487834283 radius=3.7203469240327802 gravity=190 pressure=1600 tempK=183 oxygen=false locked=false rings=true rotation=10263 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302107_-3086586_1332065 type=gasgiant mass=312.17149285060555 radius=10.911882841329781 gravity=262 pressure=1600 tempK=344 oxygen=false locked=false rings=true rotation=14381 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302109_-3086586_1332079 type=barren mass=0.0023510991180216495 radius=0.20168145982592578 gravity=6 pressure=0 tempK=266 oxygen=false locked=false rings=false rotation=20922 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302113_-3086586_1332071 type=ice mass=0.023236624649335194 radius=0.3769303000187614 gravity=16 pressure=0 tempK=177 oxygen=false locked=false rings=false rotation=16394 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302114_-3086586_1332089 type=greenhouse mass=15.24348811060426 radius=2.0413133728416963 gravity=366 pressure=1600 tempK=712 oxygen=false locked=true rings=false rotation=8527 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302116_-3086586_1332086 type=lava mass=7.532876687932706 radius=1.6663275403725344 gravity=271 pressure=49 tempK=1951 oxygen=false locked=true rings=false rotation=86063 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302118_-3086586_1332087 type=barren mass=0.013205677936849089 radius=0.3079462488366804 gravity=14 pressure=0 tempK=538 oxygen=false locked=true rings=true rotation=10129 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302121_-3086586_1332088 type=desert mass=0.1461314196694171 radius=0.6327276283379721 gravity=37 pressure=1 tempK=340 oxygen=false locked=true rings=false rotation=25342 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302129_-3086585_1332086 type=barren mass=0.0801573207690969 radius=0.5005886655353486 gravity=32 pressure=2 tempK=237 oxygen=false locked=false rings=false rotation=23399 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302140_-3086586_1332065 type=superearth mass=21.7845905298226 radius=2.3337921361694023 gravity=400 pressure=1600 tempK=317 oxygen=false locked=false rings=false rotation=6195 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302170_-3086588_1332078 type=ice mass=0.02443394862672128 radius=0.36453462415151583 gravity=18 pressure=2 tempK=92 oxygen=false locked=false rings=false rotation=41066 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302184_-3086594_1331905 type=ice mass=0.2054015059743704 radius=0.6157141695354549 gravity=54 pressure=799 tempK=93 oxygen=false locked=false rings=false rotation=40212 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1302116_-3086586_1332086 1302488_-3086594_1332155 type=gasgiant mass=255.5485641015431 radius=10.002498125514519 gravity=255 pressure=1600 tempK=84 oxygen=false locked=false rings=false rotation=6277 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1609421_-1918473_4755340 1609421_-1918473_4755340 type=barren mass=0.46770924666133595 radius=0.7827230613996494 gravity=76 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=56250 metallicity=1.5936870208421627 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1899532_3100584_4509498 1899470_3100581_4509619 type=icegiant mass=165.69415081467872 radius=8.285086710521213 gravity=241 pressure=1600 tempK=74 oxygen=false locked=false rings=true rotation=8195 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1899532_3100584_4509498 1899514_3100584_4509415 type=ice mass=0.003056167890674599 radius=0.20516492708996092 gravity=7 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=28382 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1899532_3100584_4509498 1899521_3100583_4509481 type=barren mass=0.018305767481150027 radius=0.32640763764967407 gravity=17 pressure=0 tempK=98 oxygen=false locked=false rings=false rotation=7838 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1899532_3100584_4509498 1899531_3100584_4509493 type=ice mass=0.41134681597415607 radius=0.8240496319587478 gravity=61 pressure=57 tempK=168 oxygen=false locked=true rings=false rotation=6059 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1899532_3100584_4509498 1899532_3100584_4509497 type=greenhouse mass=2.037437940104025 radius=1.278950213656414 gravity=125 pressure=234 tempK=394 oxygen=false locked=true rings=false rotation=41187 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1899532_3100584_4509498 1899532_3100584_4509498 type=lava mass=1.897278835122145 radius=1.2562049753102442 gravity=120 pressure=16 tempK=1032 oxygen=false locked=true rings=false rotation=40884 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2031833_5218371_1147521 2031833_5218371_1147521 type=ice mass=2.5510869474691416 radius=1.2501648106704106 gravity=163 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=11654 metallicity=1.4459712310561943 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2106667_5115784_6590371 2106667_5115784_6590371 type=ice mass=4.249072418266108 radius=1.4995482876777844 gravity=189 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=9721 metallicity=0.4194434083178827 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2553003_-535245_-1050940 2552999_-535245_-1050942 type=gasgiant mass=227.5677715055987 radius=9.510681930029026 gravity=252 pressure=1600 tempK=456 oxygen=false locked=false rings=true rotation=6946 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2553003_-535245_-1050940 2553001_-535245_-1050981 type=icegiant mass=166.9179971362554 radius=8.311637957512806 gravity=242 pressure=1600 tempK=141 oxygen=false locked=false rings=true rotation=9106 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2553003_-535245_-1050940 2553002_-535245_-1050920 type=ice mass=24.411665674028523 radius=2.296973618840463 gravity=400 pressure=1600 tempK=193 oxygen=false locked=false rings=false rotation=36253 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2553003_-535245_-1050940 2553003_-535245_-1050940 type=lava mass=1.900550028726414 radius=1.2839913266496088 gravity=115 pressure=13 tempK=1078 oxygen=false locked=true rings=false rotation=12040 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2553003_-535245_-1050940 2553004_-535245_-1050942 type=desert mass=0.2567533757830658 radius=0.6766914215527544 gravity=56 pressure=7 tempK=320 oxygen=false locked=true rings=false rotation=15749 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2553003_-535245_-1050940 2553005_-535245_-1050941 type=desert mass=1.844656909969501 radius=1.2246704181014183 gravity=123 pressure=131 tempK=367 oxygen=false locked=true rings=false rotation=95242 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2553003_-535245_-1050940 2553009_-535245_-1050938 type=barren mass=0.08530333989449732 radius=0.5304012228503437 gravity=30 pressure=3 tempK=183 oxygen=false locked=true rings=false rotation=31022 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2553003_-535245_-1050940 2553035_-535241_-1051068 type=ice mass=0.0027330751712316757 radius=0.20217275170982937 gravity=7 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=87702 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2553003_-535245_-1050940 2553062_-535246_-1050997 type=barren mass=0.017356852428609234 radius=0.3249400421494298 gravity=16 pressure=4 tempK=51 oxygen=false locked=false rings=false rotation=82116 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3411017_1872378_2686670 3411017_1872378_2686670 type=superearth mass=4.952184770820822 radius=1.603840597316356 gravity=193 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=11340 metallicity=1.1462273141153667 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3631783_-3208848_-3037694 3631783_-3208848_-3037694 type=ice mass=1.9425587511821718 radius=1.152239748699427 gravity=146 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=6687 metallicity=1.4047027502857872 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 395746_2934615_-3374968 395746_2934615_-3374968 type=barren mass=0.004214204412657573 radius=0.22103981078535706 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=75854 metallicity=0.7565338861017625 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4348163_4426685_2048679 4348163_4426685_2048679 type=ice mass=0.6475717025036999 radius=0.9424057339265934 gravity=73 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=7445 metallicity=1.2663943364164263 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4836351_4419696_6894907 4836311_4419696_6894896 type=ice mass=4.004980936282541 radius=1.544147080458167 gravity=168 pressure=1600 tempK=104 oxygen=false locked=false rings=false rotation=15044 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4836351_4419696_6894907 4836340_4419696_6894906 type=gasgiant mass=269.7218029621221 radius=10.24002265252395 gravity=257 pressure=1600 tempK=218 oxygen=false locked=false rings=true rotation=5316 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4836351_4419696_6894907 4836350_4419696_6894904 type=barren mass=0.013500375504041259 radius=0.3242170760793095 gravity=13 pressure=0 tempK=217 oxygen=false locked=true rings=false rotation=20318 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4836351_4419696_6894907 4836351_4419696_6894902 type=gasgiant mass=21.393810118666952 radius=3.402256404961512 gravity=185 pressure=1600 tempK=317 oxygen=false locked=false rings=true rotation=11193 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4836351_4419696_6894907 4836351_4419696_6894907 type=barren mass=0.002482848571723704 radius=0.20323854566294994 gravity=6 pressure=0 tempK=843 oxygen=false locked=true rings=false rotation=46184 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4836351_4419696_6894907 4836351_4419696_6894909 type=barren mass=0.0027190919055376955 radius=0.20845924847231542 gravity=6 pressure=0 tempK=281 oxygen=false locked=true rings=false rotation=46871 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4836351_4419696_6894907 4836371_4419695_6894919 type=barren mass=0.016383987712612607 radius=0.3528799763588127 gravity=13 pressure=3 tempK=75 oxygen=false locked=false rings=false rotation=25396 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4836351_4419696_6894907 4836414_4419699_6894927 type=superearth mass=21.736906495855145 radius=2.188905473138211 gravity=400 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=69924 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5393152_-2277199_2439904 5393106_-2277200_2439916 type=icegiant mass=272.39323545820196 radius=10.283996064694179 gravity=258 pressure=1600 tempK=122 oxygen=false locked=false rings=true rotation=8051 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5393152_-2277199_2439904 5393136_-2277200_2439909 type=icegiant mass=107.27157898441754 radius=6.858044787609019 gravity=228 pressure=1600 tempK=208 oxygen=false locked=false rings=true rotation=6030 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5393152_-2277199_2439904 5393151_-2277199_2439895 type=superearth mass=19.492106006194042 radius=2.186045391780912 gravity=400 pressure=1600 tempK=305 oxygen=false locked=false rings=false rotation=13463 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5393152_-2277199_2439904 5393152_-2277199_2439904 type=lava mass=1.0396455318374358 radius=1.0476581912821665 gravity=95 pressure=2 tempK=1011 oxygen=false locked=true rings=false rotation=79360 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5393152_-2277199_2439904 5393153_-2277199_2439905 type=greenhouse mass=2.0705829669089 radius=1.2469482547976387 gravity=133 pressure=437 tempK=398 oxygen=false locked=true rings=false rotation=49207 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5393152_-2277199_2439904 5393160_-2277199_2439900 type=ice mass=0.8085225439795893 radius=0.9514122875049971 gravity=89 pressure=155 tempK=152 oxygen=false locked=true rings=false rotation=6190 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5393152_-2277199_2439904 5393200_-2277200_2439964 type=ice mass=6.911285732211827 radius=1.6276446283068091 gravity=261 pressure=1600 tempK=91 oxygen=false locked=false rings=false rotation=7608 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5854133_275080_2604160 5854133_275080_2604160 type=ice mass=3.580183683247193 radius=1.400785633512131 gravity=182 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=48181 metallicity=0.6292643178557435 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5891934_-757386_5055698 5891934_-757386_5055698 type=superearth mass=12.048504252431522 radius=1.893272712804346 gravity=336 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=14494 metallicity=1.3821825544882356 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6386779_6218703_-2290675 6386779_6218703_-2290675 type=ice mass=1.7551791814995448 radius=1.1355178221934288 gravity=136 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=7432 metallicity=1.0247030625294358 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6918713_2294940_-820156 6918708_2294940_-820150 type=lava mass=5.403681358397993 radius=1.6772381770058735 gravity=192 pressure=1600 tempK=712 oxygen=false locked=true rings=false rotation=8377 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6918713_2294940_-820156 6918712_2294940_-820152 type=desert mass=0.00875180556812671 radius=0.2625801459682835 gravity=13 pressure=0 tempK=415 oxygen=false locked=true rings=false rotation=91877 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6918713_2294940_-820156 6918713_2294940_-820156 type=lava mass=7.749429510714092 radius=1.8364052409673304 gravity=230 pressure=16 tempK=2075 oxygen=false locked=true rings=false rotation=34493 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6918713_2294940_-820156 6918715_2294941_-820138 type=greenhouse mass=17.249323402630562 radius=2.26108180661609 gravity=337 pressure=1600 tempK=346 oxygen=false locked=false rings=true rotation=23547 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6918713_2294940_-820156 6918743_2294939_-820129 type=superearth mass=10.27636828957459 radius=1.8296530727395168 gravity=307 pressure=1600 tempK=297 oxygen=false locked=false rings=false rotation=17054 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6918713_2294940_-820156 6918773_2294940_-820077 type=ice mass=4.391763206778984 radius=1.5512577339702143 gravity=183 pressure=1600 tempK=165 oxygen=false locked=false rings=false rotation=38168 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6918713_2294940_-820156 6918912_2294932_-820033 type=superearth mass=4.98426413590334 radius=1.5135313072630292 gravity=218 pressure=1600 tempK=123 oxygen=false locked=false rings=false rotation=15355 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6918713_2294940_-820156 6919013_2294959_-820378 type=icegiant mass=129.09205638035172 radius=7.432985633827182 gravity=234 pressure=1600 tempK=90 oxygen=false locked=false rings=true rotation=6371 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6947973_1979802_5101896 6947973_1979802_5101896 type=superearth mass=4.2554336056184825 radius=1.4038279741834425 gravity=216 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=11724 metallicity=0.41150504664655163 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 877083_5596637_-502177 type=barren mass=0.0043929527254645065 radius=0.21733584703571496 gravity=9 pressure=1 tempK=33 oxygen=false locked=false rings=false rotation=8140 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878506_5596691_-502296 type=gasgiant mass=173.30377033278236 radius=8.44842360519568 gravity=243 pressure=1600 tempK=128 oxygen=false locked=false rings=true rotation=5787 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878516_5596673_-502729 type=gasgiant mass=138.16643154342793 radius=7.655802025985872 gravity=236 pressure=1600 tempK=361 oxygen=false locked=false rings=true rotation=5793 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878534_5596672_-502627 type=ice mass=0.4329737640819448 radius=0.8168679635028027 gravity=65 pressure=71 tempK=124 oxygen=false locked=false rings=false rotation=12516 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878553_5596671_-502711 type=desert mass=1.4045176765774283 radius=1.0779650352367258 gravity=121 pressure=131 tempK=469 oxygen=false locked=false rings=false rotation=42405 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878554_5596670_-502677 type=barren mass=0.01659112176307928 radius=0.32824293768225865 gravity=15 pressure=0 tempK=248 oxygen=false locked=false rings=false rotation=8690 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878556_5596671_-502707 type=barren mass=0.34852268140098014 radius=0.7163408393411195 gravity=68 pressure=7 tempK=522 oxygen=false locked=true rings=false rotation=37661 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878558_5596671_-502734 type=desert mass=0.1311490601758935 radius=0.581475535515763 gravity=39 pressure=6 tempK=234 oxygen=false locked=false rings=false rotation=32520 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878559_5596671_-502685 type=desert mass=0.9618089904813903 radius=1.041678706292689 gravity=89 pressure=158 tempK=352 oxygen=false locked=false rings=false rotation=70372 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878562_5596671_-502705 type=lava mass=0.0022248877370953236 radius=0.20544840880774495 gravity=5 pressure=0 tempK=3109 oxygen=false locked=true rings=false rotation=10975 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878620_5596668_-502556 type=barren mass=0.006223516414818935 radius=0.2455048607815313 gravity=10 pressure=0 tempK=105 oxygen=false locked=false rings=false rotation=27645 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878732_5596624_-501739 type=icegiant mass=108.62641371674347 radius=6.895570710616032 gravity=228 pressure=1600 tempK=83 oxygen=false locked=false rings=true rotation=6989 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878562_5596671_-502705 878777_5596663_-502864 type=ice mass=9.246446677537232 radius=1.9146860220043695 gravity=252 pressure=1600 tempK=151 oxygen=false locked=false rings=false rotation=8820 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1093770_2928517_-2936619 id=-953287813 kind=STAR name=PGS--3525313.0.-3525313 starTemp=70 starSize=0.908119261264801 + system -1332546_1631083_3243750 id=-1436132233 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless + system -1502067_-1460437_4865055 id=-1542560749 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless + system -2327343_6217060_-3227882 id=-638227073 kind=STAR name=PGS--3525313.3525313.-3525313 starTemp=40 starSize=0.998796284198761 + system -3264137_6862129_5633995 id=-1878334477 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system -3272771_-2281626_-427326 id=-1787505529 kind=STAR name=PGS--3525313.-3525313.-3525313 starTemp=40 starSize=0.9442926049232483 + system -3382269_4609397_102289 id=-1804580365 kind=STAR name=PGS--3525313.3525313.0 starTemp=100 starSize=0.9761362671852112 + system -446119_-2387600_1528743 id=-1238572757 kind=STAR name=PGS--3525313.-3525313.0 starTemp=40 starSize=0.6638630628585815 + system -612264_1154834_5810641 id=-912475673 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless + system 1302116_-3086586_1332086 id=-993759433 kind=STAR name=PGS-0.-3525313.0 starTemp=70 starSize=1.0476429462432861 + system 1609421_-1918473_4755340 id=-57202221 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless + system 1899532_3100584_4509498 id=-810501533 kind=STAR name=PGS-0.0.3525313 starTemp=40 starSize=0.8984453082084656 + system 2031833_5218371_1147521 id=-770357401 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless + system 2106667_5115784_6590371 id=-714236625 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless + system 2553003_-535245_-1050940 id=-196901933 kind=STAR name=PGS-0.-3525313.-3525313 starTemp=40 starSize=0.9787595868110657 + system 3411017_1872378_2686670 id=-1958754413 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 3631783_-3208848_-3037694 id=-43961145 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless + system 395746_2934615_-3374968 id=-1652144877 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless + system 4348163_4426685_2048679 id=-1258055601 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless + system 4836351_4419696_6894907 id=-1565633077 kind=STAR name=PGS-3525313.3525313.3525313 starTemp=40 starSize=0.605627179145813 + system 5393152_-2277199_2439904 id=-893135689 kind=STAR name=PGS-3525313.-3525313.0 starTemp=40 starSize=0.862093448638916 + system 5854133_275080_2604160 id=-1515893089 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless + system 5891934_-757386_5055698 id=-1906018109 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless + system 6386779_6218703_-2290675 id=-788062221 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless + system 6918713_2294940_-820156 id=-1120847449 kind=STAR name=PGS-3525313.0.-3525313 starTemp=70 starSize=1.1846755743026733 + system 6947973_1979802_5101896 id=-1165534741 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless + system 878562_5596671_-502705 id=-1528641933 kind=STAR name=PGS-0.3525313.-3525313 starTemp=100 starSize=1.3026405572891235 seed 1337 systems=27 - body -1573709_2153652_-2170228 -1573709_2153652_-2170228 kind=ROGUE_PLANET orbit=0 radius=0.7689629830810678 starId=-1458129621 frame=true - body -2063555_7189287_7920294 -2063555_7189287_7920294 kind=MOON orbit=0 radius=1.9211387591656388 starId=-225880809 frame=false - body -2063555_7189287_7920294 -2063555_7189287_7920294 kind=ROGUE_PLANET orbit=0 radius=0.46316320420630236 starId=-225880809 frame=true - body -2140810_2039107_9279683 -2140810_2039107_9279683 kind=ROGUE_PLANET orbit=0 radius=0.2085263508076044 starId=-243099345 frame=true - body -3336357_4475214_3033351 -3336357_4475214_3033351 kind=MOON orbit=0 radius=1.2087626383751437 starId=-629791769 frame=false - body -3336357_4475214_3033351 -3336357_4475214_3033351 kind=MOON orbit=0 radius=1.5941117666200255 starId=-629791769 frame=false - body -3336357_4475214_3033351 -3336357_4475214_3033351 kind=ROGUE_PLANET orbit=0 radius=0.6229314360348599 starId=-629791769 frame=true - body -364554_6854024_-3395136 -364554_6854024_-3395136 kind=MOON orbit=0 radius=0.3810385920443371 starId=-180255469 frame=false - body -364554_6854024_-3395136 -364554_6854024_-3395136 kind=ROGUE_PLANET orbit=0 radius=2.252290185189226 starId=-180255469 frame=true - body -3941235_-1709038_3545575 -3941235_-1709038_3545575 kind=MOON orbit=0 radius=0.6196082002556256 starId=-1370886497 frame=false - body -3941235_-1709038_3545575 -3941235_-1709038_3545575 kind=ROGUE_PLANET orbit=0 radius=1.1985513611208807 starId=-1370886497 frame=true - body -4666123_-580567_5682104 -4666123_-580567_5682104 kind=MOON orbit=0 radius=2.1715494207305213 starId=-1303466537 frame=false - body -4666123_-580567_5682104 -4666123_-580567_5682104 kind=MOON orbit=0 radius=2.3151726622408 starId=-1303466537 frame=false - body -4666123_-580567_5682104 -4666123_-580567_5682104 kind=ROGUE_PLANET orbit=0 radius=0.40772533876055145 starId=-1303466537 frame=true - body -554372_-4442061_-774973 -554372_-4442061_-774973 kind=MOON orbit=0 radius=0.4396589713261482 starId=-291544557 frame=false - body -554372_-4442061_-774973 -554372_-4442061_-774973 kind=MOON orbit=0 radius=1.1299659557046622 starId=-291544557 frame=false - body -554372_-4442061_-774973 -554372_-4442061_-774973 kind=ROGUE_PLANET orbit=0 radius=2.0544146928745537 starId=-291544557 frame=true - body -626941_5154507_4503681 -626941_5154507_4503681 kind=MOON orbit=0 radius=0.9887336142199961 starId=-789817773 frame=false - body -626941_5154507_4503681 -626941_5154507_4503681 kind=ROGUE_PLANET orbit=0 radius=1.687645291523904 starId=-789817773 frame=true - body 1228651_-2557618_8861204 1228651_-2557618_8861204 kind=MOON orbit=0 radius=0.6899148440246614 starId=-548985509 frame=false - body 1228651_-2557618_8861204 1228651_-2557618_8861204 kind=MOON orbit=0 radius=0.8075854491022794 starId=-548985509 frame=false - body 1228651_-2557618_8861204 1228651_-2557618_8861204 kind=ROGUE_PLANET orbit=0 radius=2.0824746625433077 starId=-548985509 frame=true - body 2274979_7150205_-1755432 2274979_7150205_-1755432 kind=ROGUE_PLANET orbit=0 radius=0.6413676617295493 starId=-840467989 frame=true - body 2691671_5432625_7859903 2691587_5432625_7859996 kind=STAR orbit=670 radius=91.48043859779835 starId=-1879670171 frame=true - body 2691671_5432625_7859903 2691671_5432625_7859903 kind=STAR orbit=0 radius=0.0 starId=-1879670169 frame=true - body 2691671_5432625_7859903 2691672_5432625_7859909 kind=STAR orbit=32 radius=91.56488044381142 starId=-1879670170 frame=true - body 2691671_5432625_7859903 2691672_5432625_7859923 kind=PLANET orbit=106 radius=1.073167880211255 starId=-1879670169 frame=true - body 2691671_5432625_7859903 2691701_5432626_7859911 kind=ASTEROID_BELT orbit=169 radius=0.0 starId=-1879670169 frame=true - body 3486529_6795101_617493 3486529_6795101_617493 kind=ROGUE_PLANET orbit=0 radius=2.367000742190591 starId=-1428201685 frame=true - body 3662998_191651_-1822417 3662998_191651_-1822417 kind=MOON orbit=0 radius=1.147549354034543 starId=-41240637 frame=false - body 3662998_191651_-1822417 3662998_191651_-1822417 kind=ROGUE_PLANET orbit=0 radius=2.3848465086399933 starId=-41240637 frame=true - body 3744070_1703327_2446882 3744070_1703327_2446882 kind=ROGUE_PLANET orbit=0 radius=1.1019636237102468 starId=-342178325 frame=true - body 4347096_3736058_9064309 4347096_3736058_9064309 kind=ROGUE_PLANET orbit=0 radius=0.3183726327938388 starId=-439396409 frame=true - body 528043_-2545788_1919161 528043_-2545788_1919161 kind=MOON orbit=0 radius=2.31808440045958 starId=-1278118149 frame=false - body 528043_-2545788_1919161 528043_-2545788_1919161 kind=MOON orbit=0 radius=2.3864950791588417 starId=-1278118149 frame=false - body 528043_-2545788_1919161 528043_-2545788_1919161 kind=ROGUE_PLANET orbit=0 radius=0.7363821812286615 starId=-1278118149 frame=true - body 5948611_6639061_7642905 5948611_6639061_7642905 kind=MOON orbit=0 radius=0.2899400912366618 starId=-775167053 frame=false - body 5948611_6639061_7642905 5948611_6639061_7642905 kind=MOON orbit=0 radius=0.35359605598253835 starId=-775167053 frame=false - body 5948611_6639061_7642905 5948611_6639061_7642905 kind=ROGUE_PLANET orbit=0 radius=1.8870973054918487 starId=-775167053 frame=true - body 6681053_-1258075_6889516 6681053_-1258075_6889516 kind=MOON orbit=0 radius=0.2135741271197545 starId=-557819189 frame=false - body 6681053_-1258075_6889516 6681053_-1258075_6889516 kind=MOON orbit=0 radius=1.597241773129025 starId=-557819189 frame=false - body 6681053_-1258075_6889516 6681053_-1258075_6889516 kind=ROGUE_PLANET orbit=0 radius=0.3754924381667063 starId=-557819189 frame=true - body 6822102_5146281_2692793 6822102_5146281_2692793 kind=MOON orbit=0 radius=0.2558111354762624 starId=-652918041 frame=false - body 6822102_5146281_2692793 6822102_5146281_2692793 kind=ROGUE_PLANET orbit=0 radius=0.24605547826027752 starId=-652918041 frame=true - body 7474804_-1243737_1670574 7474804_-1243737_1670574 kind=MOON orbit=0 radius=0.9621043876085937 starId=-176624233 frame=false - body 7474804_-1243737_1670574 7474804_-1243737_1670574 kind=MOON orbit=0 radius=1.5767149252404509 starId=-176624233 frame=false - body 7474804_-1243737_1670574 7474804_-1243737_1670574 kind=ROGUE_PLANET orbit=0 radius=1.0177477139653435 starId=-176624233 frame=true - body 756689_-1996515_-853506 756689_-1996515_-853506 kind=MOON orbit=0 radius=1.5652194226482485 starId=-1395994849 frame=false - body 756689_-1996515_-853506 756689_-1996515_-853506 kind=MOON orbit=0 radius=2.4105363035989726 starId=-1395994849 frame=false - body 756689_-1996515_-853506 756689_-1996515_-853506 kind=ROGUE_PLANET orbit=0 radius=0.32387786813174946 starId=-1395994849 frame=true - body 7769045_2585852_903758 7769002_2585853_903885 kind=ASTEROID_BELT orbit=718 radius=0.0 starId=-1675040469 frame=true - body 7769045_2585852_903758 7769003_2585851_903765 kind=PLANET orbit=227 radius=0.48703923768915025 starId=-1675040469 frame=true - body 7769045_2585852_903758 7769033_2585852_903752 kind=PLANET orbit=70 radius=2.4851009831751356 starId=-1675040469 frame=true - body 7769045_2585852_903758 7769038_2585852_903761 kind=PLANET orbit=41 radius=0.7871550612532745 starId=-1675040469 frame=true - body 7769045_2585852_903758 7769044_2585852_903763 kind=PLANET orbit=27 radius=0.382747324712566 starId=-1675040469 frame=true - body 7769045_2585852_903758 7769045_2585852_903757 kind=PLANET orbit=8 radius=1.0470728760292016 starId=-1675040469 frame=true - body 7769045_2585852_903758 7769045_2585852_903758 kind=STAR orbit=0 radius=0.0 starId=-1675040469 frame=true - body 7769045_2585852_903758 7769047_2585852_903756 kind=PLANET orbit=15 radius=1.3656342123398215 starId=-1675040469 frame=true - body 7769045_2585852_903758 7769065_2585851_903737 kind=MOON orbit=155 radius=0.21152405129733184 starId=-1675040469 frame=false - body 7769045_2585852_903758 7769065_2585851_903737 kind=PLANET orbit=155 radius=0.27688238134245285 starId=-1675040469 frame=true - body 7769045_2585852_903758 7769090_2585855_903687 kind=PLANET orbit=449 radius=0.4262581479440023 starId=-1675040469 frame=true - body 8273292_573611_-2696544 8273292_573611_-2696544 kind=MOON orbit=0 radius=0.288726105158068 starId=-1739755397 frame=false - body 8273292_573611_-2696544 8273292_573611_-2696544 kind=ROGUE_PLANET orbit=0 radius=0.43887128385988716 starId=-1739755397 frame=true - body 8446414_4393296_7956124 8446414_4393296_7956124 kind=ROGUE_PLANET orbit=0 radius=2.191233610427466 starId=-264084353 frame=true - body 8839150_8100443_-3684508 8839150_8100443_-3684508 kind=MOON orbit=0 radius=0.40758051197899203 starId=-91668937 frame=false - body 8839150_8100443_-3684508 8839150_8100443_-3684508 kind=MOON orbit=0 radius=1.0607923338311969 starId=-91668937 frame=false - body 8839150_8100443_-3684508 8839150_8100443_-3684508 kind=ROGUE_PLANET orbit=0 radius=0.31270392841251154 starId=-91668937 frame=true - body 8988904_-910550_-4036263 8988904_-910550_-4036263 kind=MOON orbit=0 radius=1.0074973919978136 starId=-1207384369 frame=false - body 8988904_-910550_-4036263 8988904_-910550_-4036263 kind=ROGUE_PLANET orbit=0 radius=1.1458517936303287 starId=-1207384369 frame=true - derived -1573709_2153652_-2170228 -1573709_2153652_-2170228 type=barren mass=0.32414223439268397 radius=0.7689629830810678 gravity=55 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=7632 metallicity=1.4554282434374612 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2063555_7189287_7920294 -2063555_7189287_7920294 type=ice mass=0.06051953130553028 radius=0.46316320420630236 gravity=28 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=37697 metallicity=0.6844507656397616 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2140810_2039107_9279683 -2140810_2039107_9279683 type=barren mass=0.002751983861320743 radius=0.2085263508076044 gravity=6 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=32283 metallicity=1.2079601860651414 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3336357_4475214_3033351 -3336357_4475214_3033351 type=ice mass=0.16019483571353615 radius=0.6229314360348599 gravity=41 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=11669 metallicity=0.5158513859142492 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -364554_6854024_-3395136 -364554_6854024_-3395136 type=ice mass=20.83809474201407 radius=2.252290185189226 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=8566 metallicity=0.8424423233343017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3941235_-1709038_3545575 -3941235_-1709038_3545575 type=ice mass=2.233324231141556 radius=1.1985513611208807 gravity=155 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=21930 metallicity=1.4897595589858756 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4666123_-580567_5682104 -4666123_-580567_5682104 type=barren mass=0.03317275208369183 radius=0.40772533876055145 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=41584 metallicity=1.0115030110151066 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -554372_-4442061_-774973 -554372_-4442061_-774973 type=ice mass=17.253752883812503 radius=2.0544146928745537 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=9670 metallicity=0.9905132772116106 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -626941_5154507_4503681 -626941_5154507_4503681 type=ice mass=8.39157323910817 radius=1.687645291523904 gravity=295 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=34181 metallicity=1.0878429053553482 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1228651_-2557618_8861204 1228651_-2557618_8861204 type=ice mass=14.548057887563953 radius=2.0824746625433077 gravity=335 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=6353 metallicity=1.365480786121032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2274979_7150205_-1755432 2274979_7150205_-1755432 type=ice mass=0.1500505517262651 radius=0.6413676617295493 gravity=36 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=13849 metallicity=1.2204472319378885 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2691671_5432625_7859903 2691587_5432625_7859996 type=barren mass=0.026654851089978983 radius=0.36753897512388545 gravity=20 pressure=8 tempK=48 oxygen=false locked=false rings=false rotation=11652 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2691671_5432625_7859903 2691671_5432625_7859903 type=lava mass=21.046812853607555 radius=2.4009953470502263 gravity=365 pressure=444 tempK=1630 oxygen=false locked=true rings=false rotation=16494 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2691671_5432625_7859903 2691672_5432625_7859909 type=exotic mass=0.9593281688075637 radius=0.9777208710388154 gravity=100 pressure=633 tempK=327 oxygen=false locked=true rings=false rotation=51981 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2691671_5432625_7859903 2691672_5432625_7859923 type=exotic mass=1.329896361560754 radius=1.073167880211255 gravity=115 pressure=1600 tempK=241 oxygen=false locked=false rings=false rotation=57635 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2691671_5432625_7859903 2691701_5432626_7859911 type=gasgiant mass=259.3928856424584 radius=10.067644615631313 gravity=256 pressure=1600 tempK=177 oxygen=false locked=false rings=true rotation=9821 metallicity=0.6042757298977723 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3486529_6795101_617493 3486529_6795101_617493 type=superearth mass=28.824756109067664 radius=2.367000742190591 gravity=400 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=61701 metallicity=0.851184826279871 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3662998_191651_-1822417 3662998_191651_-1822417 type=ice mass=29.702940798839204 radius=2.3848465086399933 gravity=400 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=9412 metallicity=1.0900309737699718 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3744070_1703327_2446882 3744070_1703327_2446882 type=ice mass=1.2538592333737375 radius=1.1019636237102468 gravity=103 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=33387 metallicity=1.0770181111183947 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4347096_3736058_9064309 4347096_3736058_9064309 type=barren mass=0.01194221705201743 radius=0.3183726327938388 gravity=12 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=42431 metallicity=0.8600542573611804 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 528043_-2545788_1919161 528043_-2545788_1919161 type=ice mass=0.26662901782290777 radius=0.7363821812286615 gravity=49 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=90972 metallicity=1.0255548888407957 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5948611_6639061_7642905 5948611_6639061_7642905 type=superearth mass=9.150169015332779 radius=1.8870973054918487 gravity=257 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=10868 metallicity=0.3794022609927985 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6681053_-1258075_6889516 6681053_-1258075_6889516 type=barren mass=0.028288794565751216 radius=0.3754924381667063 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=33130 metallicity=0.4759672475361737 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6822102_5146281_2692793 6822102_5146281_2692793 type=barren mass=0.004813129534930833 radius=0.24605547826027752 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=6712 metallicity=0.6084353447517761 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7474804_-1243737_1670574 7474804_-1243737_1670574 type=ice mass=1.259889576391659 radius=1.0177477139653435 gravity=122 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=79353 metallicity=0.7933269028516394 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 756689_-1996515_-853506 756689_-1996515_-853506 type=ice mass=0.016648914723364397 radius=0.32387786813174946 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=15800 metallicity=1.23751877539955 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769002_2585853_903885 type=gasgiant mass=91.5463181268455 radius=6.401300779274569 gravity=223 pressure=1600 tempK=74 oxygen=false locked=false rings=true rotation=8210 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769003_2585851_903765 type=ice mass=0.057333353032806525 radius=0.48703923768915025 gravity=24 pressure=11 tempK=55 oxygen=false locked=false rings=false rotation=8333 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769033_2585852_903752 type=superearth mass=32.93467878722113 radius=2.4851009831751356 gravity=400 pressure=1600 tempK=259 oxygen=false locked=false rings=false rotation=49878 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769038_2585852_903761 type=ice mass=0.3697306078750307 radius=0.7871550612532745 gravity=60 pressure=83 tempK=140 oxygen=false locked=true rings=false rotation=15596 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769044_2585852_903763 type=barren mass=0.03096611566502426 radius=0.382747324712566 gravity=21 pressure=1 tempK=196 oxygen=false locked=true rings=false rotation=41416 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769045_2585852_903757 type=greenhouse mass=1.4046047539899567 radius=1.0470728760292016 gravity=128 pressure=247 tempK=371 oxygen=false locked=true rings=false rotation=17124 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769045_2585852_903758 type=lava mass=1.5088556417880772 radius=1.0678552850270082 gravity=132 pressure=19 tempK=1026 oxygen=false locked=true rings=false rotation=16998 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769047_2585852_903756 type=greenhouse mass=2.7852326393056224 radius=1.3656342123398215 gravity=149 pressure=1600 tempK=433 oxygen=false locked=true rings=false rotation=43075 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769065_2585851_903737 type=ice mass=0.0082693671900693 radius=0.27688238134245285 gravity=11 pressure=1 tempK=67 oxygen=false locked=false rings=false rotation=11817 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7769045_2585852_903758 7769090_2585855_903687 type=barren mass=0.03545481939276029 radius=0.4262581479440023 gravity=20 pressure=7 tempK=48 oxygen=false locked=false rings=false rotation=67577 metallicity=0.9416054888520873 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8273292_573611_-2696544 8273292_573611_-2696544 type=ice mass=0.044845367952230576 radius=0.43887128385988716 gravity=23 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=18491 metallicity=0.42756566034063687 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8446414_4393296_7956124 8446414_4393296_7956124 type=superearth mass=20.256977086630656 radius=2.191233610427466 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=14500 metallicity=1.0258335666185951 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8839150_8100443_-3684508 8839150_8100443_-3684508 type=barren mass=0.015379281393746926 radius=0.31270392841251154 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=15514 metallicity=1.2063989283574807 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8988904_-910550_-4036263 8988904_-910550_-4036263 type=ice mass=1.9383555642911707 radius=1.1458517936303287 gravity=148 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=46415 metallicity=1.3865836729416734 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1573709_2153652_-2170228 id=-1458129621 kind=ROGUE_PLANET name=PGR--5002361.0.-5002361 starless - system -2063555_7189287_7920294 id=-225880809 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless - system -2140810_2039107_9279683 id=-243099345 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless - system -3336357_4475214_3033351 id=-629791769 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless - system -364554_6854024_-3395136 id=-180255469 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless - system -3941235_-1709038_3545575 id=-1370886497 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless - system -4666123_-580567_5682104 id=-1303466537 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless - system -554372_-4442061_-774973 id=-291544557 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless - system -626941_5154507_4503681 id=-789817773 kind=ROGUE_PLANET name=PGR--5002361.5002361.0 starless - system 1228651_-2557618_8861204 id=-548985509 kind=ROGUE_PLANET name=PGR-0.-5002361.5002361 starless - system 2274979_7150205_-1755432 id=-840467989 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless - system 2691671_5432625_7859903 id=-1879670169 kind=STAR name=PGS-0.5002361.5002361 starTemp=40 starSize=0.8387366533279419 - system 3486529_6795101_617493 id=-1428201685 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless - system 3662998_191651_-1822417 id=-41240637 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless - system 3744070_1703327_2446882 id=-342178325 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 4347096_3736058_9064309 id=-439396409 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless - system 528043_-2545788_1919161 id=-1278118149 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless - system 5948611_6639061_7642905 id=-775167053 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless - system 6681053_-1258075_6889516 id=-557819189 kind=ROGUE_PLANET name=PGR-5002361.-5002361.5002361 starless - system 6822102_5146281_2692793 id=-652918041 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless - system 7474804_-1243737_1670574 id=-176624233 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless - system 756689_-1996515_-853506 id=-1395994849 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless - system 7769045_2585852_903758 id=-1675040469 kind=STAR name=PGS-5002361.0.0 starTemp=40 starSize=0.8881509304046631 - system 8273292_573611_-2696544 id=-1739755397 kind=ROGUE_PLANET name=PGR-5002361.0.-5002361 starless - system 8446414_4393296_7956124 id=-264084353 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless - system 8839150_8100443_-3684508 id=-91668937 kind=ROGUE_PLANET name=PGR-5002361.5002361.-5002361 starless - system 8988904_-910550_-4036263 id=-1207384369 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless + body -1923947_-1394043_-2837349 -1923947_-1394043_-2837349 kind=MOON orbit=0 radius=0.3504343535747473 starId=-1836287757 frame=false at=45262,0,21712 + body -1923947_-1394043_-2837349 -1923947_-1394043_-2837349 kind=MOON orbit=0 radius=1.0576569744419213 starId=-1836287757 frame=false at=112401,0,-19027 + body -1923947_-1394043_-2837349 -1923947_-1394043_-2837349 kind=ROGUE_PLANET orbit=0 radius=0.5105195562020988 starId=-1836287757 frame=true at=0,0,0 + body -2547136_6553750_-2327906 -2547136_6553750_-2327906 kind=MOON orbit=0 radius=0.900273147073968 starId=-384211249 frame=false at=112983,0,-147500 + body -2547136_6553750_-2327906 -2547136_6553750_-2327906 kind=MOON orbit=0 radius=1.85660762023691 starId=-384211249 frame=false at=-6167,0,-88786 + body -2547136_6553750_-2327906 -2547136_6553750_-2327906 kind=ROGUE_PLANET orbit=0 radius=0.9094368622429794 starId=-384211249 frame=true at=0,0,0 + body -3335279_-2608304_4283952 -3335279_-2608304_4283952 kind=ROGUE_PLANET orbit=0 radius=1.052788722587457 starId=-783595273 frame=true at=0,0,0 + body -3347275_169917_-2040789 -3347275_169917_-2040789 kind=MOON orbit=0 radius=0.41891985661068964 starId=-1080499465 frame=false at=-338885,0,126140 + body -3347275_169917_-2040789 -3347275_169917_-2040789 kind=ROGUE_PLANET orbit=0 radius=1.9202731970800502 starId=-1080499465 frame=true at=0,0,0 + body -3350400_5100916_4798244 -3350400_5100916_4798244 kind=ROGUE_PLANET orbit=0 radius=0.605114195212058 starId=-513667701 frame=true at=0,0,0 + body -428001_4188003_1035570 -428001_4188003_1035570 kind=ROGUE_PLANET orbit=0 radius=1.1126961933492066 starId=-1929605705 frame=true at=0,0,0 + body -429399_-975487_1152563 -428852_-975510_1152659 kind=ASTEROID_BELT orbit=2972 radius=0.0 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429309_-975487_1152547 kind=GAS_GIANT orbit=488 radius=9.868809637033028 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429309_-975487_1152547 kind=MOON orbit=488 radius=0.500451212795628 starId=-751569025 frame=false at=516972,0,2941517 + body -429399_-975487_1152563 -429373_-975485_1152519 kind=ASTEROID_BELT orbit=271 radius=0.0 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429376_-975486_1152564 kind=MOON orbit=125 radius=0.20907843201502585 starId=-751569025 frame=false at=14863,0,-36489 + body -429399_-975487_1152563 -429376_-975486_1152564 kind=PLANET orbit=125 radius=0.3074005137389715 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429390_-975487_1152605 kind=MOON orbit=232 radius=0.24326546571335786 starId=-751569025 frame=false at=-28759,0,-56502 + body -429399_-975487_1152563 -429390_-975487_1152605 kind=MOON orbit=232 radius=0.7421202957180757 starId=-751569025 frame=false at=41961,0,-29338 + body -429399_-975487_1152563 -429390_-975487_1152605 kind=PLANET orbit=232 radius=0.325630568968772 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429399_-975487_1152563 kind=STAR orbit=0 radius=0.0 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429401_-975487_1152561 kind=PLANET orbit=14 radius=1.7488084514421833 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429404_-975487_1152562 kind=PLANET orbit=27 radius=0.7162074824220463 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429404_-975487_1152573 kind=PLANET orbit=58 radius=0.7218413062090878 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429544_-975483_1152487 kind=MOON orbit=877 radius=0.49083928585913217 starId=-751569025 frame=false at=-82351,0,293672 + body -429399_-975487_1152563 -429544_-975483_1152487 kind=PLANET orbit=877 radius=1.8057296310075321 starId=-751569025 frame=true at=0,0,0 + body -429399_-975487_1152563 -429718_-975476_1152699 kind=MOON orbit=1858 radius=0.24438618987929492 starId=-751569025 frame=false at=-11670,0,99317 + body -429399_-975487_1152563 -429718_-975476_1152699 kind=PLANET orbit=1858 radius=0.3279921891899943 starId=-751569025 frame=true at=0,0,0 + body -565986_1972460_5527035 -565986_1972460_5527035 kind=MOON orbit=0 radius=0.20311774682099143 starId=-1194177805 frame=false at=37385,0,11790 + body -565986_1972460_5527035 -565986_1972460_5527035 kind=ROGUE_PLANET orbit=0 radius=0.20510853917918126 starId=-1194177805 frame=true at=0,0,0 + body -810232_3115510_2746902 -810232_3115510_2746902 kind=ROGUE_PLANET orbit=0 radius=1.7208896712788562 starId=-1645051273 frame=true at=0,0,0 + body 1713158_-2537384_2172172 1713158_-2537384_2172172 kind=ROGUE_PLANET orbit=0 radius=0.7718606386627891 starId=-1753285501 frame=true at=0,0,0 + body 1737362_-2563157_5310115 1737362_-2563157_5310115 kind=ROGUE_PLANET orbit=0 radius=0.24063193054985418 starId=-1864487113 frame=true at=0,0,0 + body 237817_5592419_4203270 237817_5592419_4203270 kind=ROGUE_PLANET orbit=0 radius=1.4298526622976593 starId=-1978443773 frame=true at=0,0,0 + body 2414414_3713625_-2289058 2414414_3713625_-2289058 kind=ROGUE_PLANET orbit=0 radius=0.8921379780825387 starId=-352841721 frame=true at=0,0,0 + body 2528228_-231370_-794020 2528228_-231370_-794020 kind=ROGUE_PLANET orbit=0 radius=1.6503145995487494 starId=-1916009805 frame=true at=0,0,0 + body 2663742_1091380_-601497 2663369_1091393_-601424 kind=ASTEROID_BELT orbit=2032 radius=0.0 starId=-824346553 frame=true at=0,0,0 + body 2663742_1091380_-601497 2663727_1091381_-601507 kind=ASTEROID_BELT orbit=97 radius=0.0 starId=-824346553 frame=true at=0,0,0 + body 2663742_1091380_-601497 2663742_1091380_-601497 kind=STAR orbit=0 radius=0.0 starId=-824346553 frame=true at=0,0,0 + body 2663742_1091380_-601497 2663746_1091380_-601495 kind=PLANET orbit=22 radius=0.654463219883428 starId=-824346553 frame=true at=0,0,0 + body 2663742_1091380_-601497 2663756_1091380_-601527 kind=GAS_GIANT orbit=176 radius=7.03735373269887 starId=-824346553 frame=true at=0,0,0 + body 2663742_1091380_-601497 2663946_1091379_-601618 kind=MOON orbit=1270 radius=0.6340384622197031 starId=-824346553 frame=false at=6458,0,-37650 + body 2663742_1091380_-601497 2663946_1091379_-601618 kind=PLANET orbit=1270 radius=0.3669832544972964 starId=-824346553 frame=true at=0,0,0 + body 2680390_5368406_2491985 2680390_5368406_2491985 kind=MOON orbit=0 radius=0.2657251222699931 starId=-1809354021 frame=false at=-26076,0,129807 + body 2680390_5368406_2491985 2680390_5368406_2491985 kind=MOON orbit=0 radius=0.3621595202802164 starId=-1809354021 frame=false at=-118064,0,-148865 + body 2680390_5368406_2491985 2680390_5368406_2491985 kind=ROGUE_PLANET orbit=0 radius=0.6291674918965895 starId=-1809354021 frame=true at=0,0,0 + body 2779562_651884_2693855 2779562_651884_2693855 kind=MOON orbit=0 radius=1.8740284163883787 starId=-342178325 frame=false at=114312,0,306369 + body 2779562_651884_2693855 2779562_651884_2693855 kind=ROGUE_PLANET orbit=0 radius=1.5977631514755333 starId=-342178325 frame=true at=0,0,0 + body 3951440_4496837_903895 3951440_4496837_903895 kind=ROGUE_PLANET orbit=0 radius=0.2824811542361627 starId=-610246349 frame=true at=0,0,0 + body 4245760_1904115_3414082 4245760_1904115_3414082 kind=MOON orbit=0 radius=0.7516842566655517 starId=-847248597 frame=false at=23114,0,246117 + body 4245760_1904115_3414082 4245760_1904115_3414082 kind=MOON orbit=0 radius=1.855151261146064 starId=-847248597 frame=false at=-98334,0,69474 + body 4245760_1904115_3414082 4245760_1904115_3414082 kind=ROGUE_PLANET orbit=0 radius=1.1983557194239915 starId=-847248597 frame=true at=0,0,0 + body 4758850_1796822_6525935 4758850_1796822_6525935 kind=ROGUE_PLANET orbit=0 radius=2.430158213292819 starId=-1218084841 frame=true at=0,0,0 + body 4871731_-1898639_-2858414 4871373_-1898655_-2858243 kind=PLANET orbit=2123 radius=0.20276781582524947 starId=-782061137 frame=true at=0,0,0 + body 4871731_-1898639_-2858414 4871730_-1898639_-2858397 kind=PLANET orbit=89 radius=1.1128743239382588 starId=-782061137 frame=true at=0,0,0 + body 4871731_-1898639_-2858414 4871731_-1898639_-2858414 kind=STAR orbit=0 radius=0.0 starId=-782061137 frame=true at=0,0,0 + body 4871731_-1898639_-2858414 4871733_-1898639_-2858412 kind=STAR orbit=16 radius=86.23844600737095 starId=-782061138 frame=true at=0,0,0 + body 4871731_-1898639_-2858414 4871781_-1898639_-2858386 kind=ASTEROID_BELT orbit=305 radius=0.0 starId=-782061137 frame=true at=0,0,0 + body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=GAS_GIANT orbit=549 radius=6.469918942207257 starId=-782061137 frame=true at=0,0,0 + body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=MOON orbit=549 radius=0.243831978323108 starId=-782061137 frame=false at=-128192,0,-659660 + body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=MOON orbit=549 radius=0.2638527372015568 starId=-782061137 frame=false at=-1532887,0,-626572 + body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=MOON orbit=549 radius=0.2771452170699142 starId=-782061137 frame=false at=-1264281,0,-176920 + body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=MOON orbit=549 radius=0.45642043423103856 starId=-782061137 frame=false at=1368418,0,65829 + body 4871731_-1898639_-2858414 4871865_-1898612_-2859034 kind=ASTEROID_BELT orbit=3396 radius=0.0 starId=-782061137 frame=true at=0,0,0 + body 5051436_4723973_4799957 5051436_4723973_4799957 kind=ROGUE_PLANET orbit=0 radius=0.3044740134514678 starId=-1829532925 frame=true at=0,0,0 + body 5111326_6618461_-1069601 5111326_6618461_-1069601 kind=MOON orbit=0 radius=1.2042860777026476 starId=-1405460665 frame=false at=-36390,0,29745 + body 5111326_6618461_-1069601 5111326_6618461_-1069601 kind=MOON orbit=0 radius=1.3942527248014411 starId=-1405460665 frame=false at=6387,0,-43736 + body 5111326_6618461_-1069601 5111326_6618461_-1069601 kind=ROGUE_PLANET orbit=0 radius=0.25203985524006844 starId=-1405460665 frame=true at=0,0,0 + body 525712_1170379_4858268 525712_1170379_4858268 kind=MOON orbit=0 radius=1.4063633002353326 starId=-1715483833 frame=false at=203859,0,278576 + body 525712_1170379_4858268 525712_1170379_4858268 kind=MOON orbit=0 radius=1.4846251172749703 starId=-1715483833 frame=false at=64975,0,-181309 + body 525712_1170379_4858268 525712_1170379_4858268 kind=ROGUE_PLANET orbit=0 radius=1.478514845293077 starId=-1715483833 frame=true at=0,0,0 + body 6058391_-999455_4262104 6058391_-999455_4262104 kind=ROGUE_PLANET orbit=0 radius=0.23889752716043286 starId=-354433101 frame=true at=0,0,0 + body 6359032_-1380151_2520214 6359032_-1380151_2520214 kind=MOON orbit=0 radius=0.5435674684313252 starId=-787615481 frame=false at=339354,0,-162838 + body 6359032_-1380151_2520214 6359032_-1380151_2520214 kind=ROGUE_PLANET orbit=0 radius=1.3482517416659634 starId=-787615481 frame=true at=0,0,0 + body 6460860_1396944_-1584535 6460860_1396944_-1584535 kind=MOON orbit=0 radius=0.5938128931086055 starId=-1334375405 frame=false at=175550,0,-162183 + body 6460860_1396944_-1584535 6460860_1396944_-1584535 kind=MOON orbit=0 radius=1.3598734677567275 starId=-1334375405 frame=false at=71739,0,199706 + body 6460860_1396944_-1584535 6460860_1396944_-1584535 kind=ROGUE_PLANET orbit=0 radius=1.0717648155695128 starId=-1334375405 frame=true at=0,0,0 + derived -1923947_-1394043_-2837349 -1923947_-1394043_-2837349 type=ice mass=0.06581115549395339 radius=0.5105195562020988 gravity=25 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=10556 metallicity=1.365986142592694 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2547136_6553750_-2327906 -2547136_6553750_-2327906 type=barren mass=0.6098306725126589 radius=0.9094368622429794 gravity=74 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=22028 metallicity=0.5143935285269323 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3335279_-2608304_4283952 -3335279_-2608304_4283952 type=ice mass=0.9464517177318977 radius=1.052788722587457 gravity=85 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=9265 metallicity=0.5568980851374733 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3347275_169917_-2040789 -3347275_169917_-2040789 type=superearth mass=13.275397753273984 radius=1.9202731970800502 gravity=360 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=14424 metallicity=0.6418218811136412 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3350400_5100916_4798244 -3350400_5100916_4798244 type=ice mass=0.14297778376512693 radius=0.605114195212058 gravity=39 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=24918 metallicity=0.40432658726119813 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -428001_4188003_1035570 -428001_4188003_1035570 type=ice mass=1.1681076894397813 radius=1.1126961933492066 gravity=94 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=11050 metallicity=1.186590387662034 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -428852_-975510_1152659 type=icegiant mass=181.9187945769868 radius=8.628520970273293 gravity=244 pressure=1600 tempK=68 oxygen=false locked=false rings=false rotation=7564 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429309_-975487_1152547 type=gasgiant mass=247.76098517707226 radius=9.868809637033028 gravity=254 pressure=1600 tempK=170 oxygen=false locked=false rings=false rotation=11200 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429373_-975485_1152519 type=icegiant mass=227.78370878102936 radius=9.514604622498513 gravity=252 pressure=1600 tempK=228 oxygen=false locked=false rings=false rotation=9677 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429376_-975486_1152564 type=ice mass=0.011437734978167158 radius=0.3074005137389715 gravity=12 pressure=0 tempK=141 oxygen=false locked=false rings=false rotation=8552 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429390_-975487_1152605 type=barren mass=0.016674658861533066 radius=0.325630568968772 gravity=16 pressure=0 tempK=126 oxygen=false locked=false rings=false rotation=11877 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429399_-975487_1152563 type=lava mass=15.266327328708803 radius=2.031253823812593 gravity=370 pressure=116 tempK=2257 oxygen=false locked=true rings=false rotation=27484 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429401_-975487_1152561 type=greenhouse mass=6.884652355122905 radius=1.7488084514421833 gravity=225 pressure=1600 tempK=844 oxygen=false locked=true rings=false rotation=12787 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429404_-975487_1152562 type=barren mass=0.34239671698596114 radius=0.7162074824220463 gravity=67 pressure=18 tempK=370 oxygen=false locked=true rings=false rotation=16101 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429404_-975487_1152573 type=exotic mass=0.35764448143396327 radius=0.7218413062090878 gravity=69 pressure=54 tempK=238 oxygen=false locked=false rings=false rotation=14846 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429544_-975483_1152487 type=superearth mass=8.032522905559032 radius=1.8057296310075321 gravity=246 pressure=1600 tempK=137 oxygen=false locked=false rings=false rotation=43703 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -429399_-975487_1152563 -429718_-975476_1152699 type=barren mass=0.016085888393307872 radius=0.3279921891899943 gravity=15 pressure=1 tempK=44 oxygen=false locked=false rings=false rotation=7075 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -565986_1972460_5527035 -565986_1972460_5527035 type=barren mass=0.002875605684610644 radius=0.20510853917918126 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=95817 metallicity=0.47700164567822406 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -810232_3115510_2746902 -810232_3115510_2746902 type=superearth mass=7.360191716771793 radius=1.7208896712788562 gravity=249 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=12953 metallicity=0.4699863784583445 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1713158_-2537384_2172172 1713158_-2537384_2172172 type=ice mass=0.3756785282872785 radius=0.7718606386627891 gravity=63 pressure=0 tempK=31 oxygen=false locked=false rings=false rotation=6696 metallicity=0.6913906038924351 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1737362_-2563157_5310115 1737362_-2563157_5310115 type=barren mass=0.005260174636017469 radius=0.24063193054985418 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=16166 metallicity=1.4267140251012704 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 237817_5592419_4203270 237817_5592419_4203270 type=superearth mass=4.373813703690852 radius=1.4298526622976593 gravity=214 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=8245 metallicity=0.6855545633872082 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2414414_3713625_-2289058 2414414_3713625_-2289058 type=ice mass=0.7485232064127778 radius=0.8921379780825387 gravity=94 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=27515 metallicity=1.3289815536050376 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2528228_-231370_-794020 2528228_-231370_-794020 type=superearth mass=7.061913483941015 radius=1.6503145995487494 gravity=259 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=7871 metallicity=0.7346330420791677 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2663742_1091380_-601497 2663369_1091393_-601424 type=icegiant mass=119.99649062719153 radius=7.200575654829647 gravity=231 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=5780 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2663742_1091380_-601497 2663727_1091381_-601507 type=greenhouse mass=12.676268634867748 radius=2.137014882602409 gravity=278 pressure=1600 tempK=300 oxygen=false locked=false rings=false rotation=44982 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2663742_1091380_-601497 2663742_1091380_-601497 type=lava mass=1.7614751197982401 radius=1.099871592716175 gravity=146 pressure=7 tempK=1809 oxygen=false locked=true rings=false rotation=65128 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2663742_1091380_-601497 2663746_1091380_-601495 type=desert mass=0.22531981189321174 radius=0.654463219883428 gravity=53 pressure=2 tempK=362 oxygen=false locked=true rings=false rotation=63880 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2663742_1091380_-601497 2663756_1091380_-601527 type=gasgiant mass=113.83230272606916 radius=7.03735373269887 gravity=230 pressure=1600 tempK=264 oxygen=false locked=false rings=true rotation=8487 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2663742_1091380_-601497 2663946_1091379_-601618 type=barren mass=0.029767500903840297 radius=0.3669832544972964 gravity=22 pressure=21 tempK=50 oxygen=false locked=false rings=false rotation=63802 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2680390_5368406_2491985 2680390_5368406_2491985 type=barren mass=0.13652193362659212 radius=0.6291674918965895 gravity=34 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=87004 metallicity=1.4780569835229094 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2779562_651884_2693855 2779562_651884_2693855 type=ice mass=4.958504054661111 radius=1.5977631514755333 gravity=194 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=14650 metallicity=0.5959706790646394 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3951440_4496837_903895 3951440_4496837_903895 type=barren mass=0.007155848830710717 radius=0.2824811542361627 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=9871 metallicity=0.47319187317369416 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4245760_1904115_3414082 4245760_1904115_3414082 type=ice mass=1.8812269044999095 radius=1.1983557194239915 gravity=131 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=52117 metallicity=1.0092468005284974 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4758850_1796822_6525935 4758850_1796822_6525935 type=ice mass=32.65333761348136 radius=2.430158213292819 gravity=400 pressure=0 tempK=54 oxygen=false locked=false rings=false rotation=14123 metallicity=0.44081078770139615 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4871731_-1898639_-2858414 4871373_-1898655_-2858243 type=barren mass=0.0032792476630666718 radius=0.20276781582524947 gravity=8 pressure=0 tempK=52 oxygen=false locked=false rings=false rotation=33319 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4871731_-1898639_-2858414 4871730_-1898639_-2858397 type=ocean mass=1.5421436119125114 radius=1.1128743239382588 gravity=125 pressure=228 tempK=354 oxygen=true locked=false rings=false rotation=11651 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4871731_-1898639_-2858414 4871731_-1898639_-2858414 type=lava mass=0.06557128550471429 radius=0.49321904660895427 gravity=27 pressure=0 tempK=1003 oxygen=false locked=true rings=false rotation=23896 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4871731_-1898639_-2858414 4871733_-1898639_-2858412 type=barren mass=0.01639213260149288 radius=0.31660185583810646 gravity=16 pressure=0 tempK=512 oxygen=false locked=true rings=false rotation=10423 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4871731_-1898639_-2858414 4871781_-1898639_-2858386 type=exotic mass=1.832121060633507 radius=1.1894637898905132 gravity=129 pressure=1600 tempK=294 oxygen=false locked=false rings=false rotation=94285 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4871731_-1898639_-2858414 4871804_-1898637_-2858342 type=gasgiant mass=93.81910279069727 radius=6.469918942207257 gravity=224 pressure=1600 tempK=201 oxygen=false locked=false rings=true rotation=10743 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4871731_-1898639_-2858414 4871865_-1898612_-2859034 type=superearth mass=6.792729959242305 radius=1.6529479912454907 gravity=249 pressure=1600 tempK=88 oxygen=false locked=false rings=false rotation=27925 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5051436_4723973_4799957 5051436_4723973_4799957 type=barren mass=0.014729343074669284 radius=0.3044740134514678 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=15038 metallicity=0.35584698195458186 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5111326_6618461_-1069601 5111326_6618461_-1069601 type=barren mass=0.005438671353740839 radius=0.25203985524006844 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=60332 metallicity=1.2170486304129637 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 525712_1170379_4858268 525712_1170379_4858268 type=ice mass=3.772526885339521 radius=1.478514845293077 gravity=173 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=38853 metallicity=1.1660028429205385 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6058391_-999455_4262104 6058391_-999455_4262104 type=ice mass=0.00400535705780492 radius=0.23889752716043286 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=42845 metallicity=1.3833861048329483 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6359032_-1380151_2520214 6359032_-1380151_2520214 type=superearth mass=3.4010125266906144 radius=1.3482517416659634 gravity=187 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=41051 metallicity=1.5663099662866813 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6460860_1396944_-1584535 6460860_1396944_-1584535 type=ice mass=1.3943441840804145 radius=1.0717648155695128 gravity=121 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=12526 metallicity=0.43936845070542097 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1923947_-1394043_-2837349 id=-1836287757 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless + system -2547136_6553750_-2327906 id=-384211249 kind=ROGUE_PLANET name=PGR--3525313.3525313.-3525313 starless + system -3335279_-2608304_4283952 id=-783595273 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless + system -3347275_169917_-2040789 id=-1080499465 kind=ROGUE_PLANET name=PGR--3525313.0.-3525313 starless + system -3350400_5100916_4798244 id=-513667701 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system -428001_4188003_1035570 id=-1929605705 kind=ROGUE_PLANET name=PGR--3525313.3525313.0 starless + system -429399_-975487_1152563 id=-751569025 kind=STAR name=PGS--3525313.-3525313.0 starTemp=70 starSize=1.0282940864562988 + system -565986_1972460_5527035 id=-1194177805 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless + system -810232_3115510_2746902 id=-1645051273 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless + system 1713158_-2537384_2172172 id=-1753285501 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless + system 1737362_-2563157_5310115 id=-1864487113 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless + system 237817_5592419_4203270 id=-1978443773 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless + system 2414414_3713625_-2289058 id=-352841721 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless + system 2528228_-231370_-794020 id=-1916009805 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless + system 2663742_1091380_-601497 id=-824346553 kind=STAR name=PGS-0.0.-3525313 starTemp=70 starSize=0.9003437161445618 + system 2680390_5368406_2491985 id=-1809354021 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless + system 2779562_651884_2693855 id=-342178325 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 3951440_4496837_903895 id=-610246349 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless + system 4245760_1904115_3414082 id=-847248597 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless + system 4758850_1796822_6525935 id=-1218084841 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless + system 4871731_-1898639_-2858414 id=-782061137 kind=STAR name=PGS-3525313.-3525313.-3525313 starTemp=40 starSize=0.7899463772773743 + system 5051436_4723973_4799957 id=-1829532925 kind=ROGUE_PLANET name=PGR-3525313.3525313.3525313 starless + system 5111326_6618461_-1069601 id=-1405460665 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless + system 525712_1170379_4858268 id=-1715483833 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless + system 6058391_-999455_4262104 id=-354433101 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless + system 6359032_-1380151_2520214 id=-787615481 kind=ROGUE_PLANET name=PGR-3525313.-3525313.0 starless + system 6460860_1396944_-1584535 id=-1334375405 kind=ROGUE_PLANET name=PGR-3525313.0.-3525313 starless seed 8675309 systems=27 - body -1440306_4321771_-1644504 -1440306_4321771_-1644504 kind=MOON orbit=0 radius=0.6918311989713263 starId=-224544461 frame=false - body -1440306_4321771_-1644504 -1440306_4321771_-1644504 kind=MOON orbit=0 radius=1.4486217500512495 starId=-224544461 frame=false - body -1440306_4321771_-1644504 -1440306_4321771_-1644504 kind=ROGUE_PLANET orbit=0 radius=1.858589898702767 starId=-224544461 frame=true - body -1487091_5789006_3108422 -1487091_5789006_3108422 kind=ROGUE_PLANET orbit=0 radius=0.7622212449935264 starId=-1737974265 frame=true - body -2182872_2271086_929819 -2182872_2271086_929819 kind=ROGUE_PLANET orbit=0 radius=1.5594868722814699 starId=-852504905 frame=true - body -2532019_1322888_8152935 -2532019_1322888_8152935 kind=ROGUE_PLANET orbit=0 radius=1.95774137431041 starId=-1880339145 frame=true - body -2648150_9876469_-4697214 -2648150_9876469_-4697214 kind=MOON orbit=0 radius=0.3073465246954764 starId=-337840993 frame=false - body -2648150_9876469_-4697214 -2648150_9876469_-4697214 kind=ROGUE_PLANET orbit=0 radius=1.8158165163169306 starId=-337840993 frame=true - body -3715368_-3025898_464323 -3715067_-3025894_464264 kind=MOON orbit=1638 radius=0.20567681577882893 starId=-351188325 frame=false - body -3715368_-3025898_464323 -3715067_-3025894_464264 kind=PLANET orbit=1638 radius=1.1310343685083477 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715248_-3025896_464384 kind=PLANET orbit=718 radius=2.3765942402912463 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715284_-3025898_464806 kind=ASTEROID_BELT orbit=2620 radius=0.0 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715331_-3025899_464308 kind=ASTEROID_BELT orbit=216 radius=0.0 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715334_-3025900_464348 kind=MOON orbit=223 radius=0.47505439226056184 starId=-351188325 frame=false - body -3715368_-3025898_464323 -3715334_-3025900_464348 kind=PLANET orbit=223 radius=0.28266309604664974 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715355_-3025899_464334 kind=MOON orbit=90 radius=0.41037160072533463 starId=-351188325 frame=false - body -3715368_-3025898_464323 -3715355_-3025899_464334 kind=PLANET orbit=90 radius=2.131531437678731 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715364_-3025898_464319 kind=PLANET orbit=31 radius=2.2697636812476105 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715366_-3025898_464319 kind=PLANET orbit=22 radius=0.23651511772733647 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715368_-3025898_464323 kind=STAR orbit=0 radius=0.0 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715370_-3025898_464323 kind=MOON orbit=12 radius=0.200038276016525 starId=-351188325 frame=false - body -3715368_-3025898_464323 -3715370_-3025898_464323 kind=MOON orbit=12 radius=0.21441160139738466 starId=-351188325 frame=false - body -3715368_-3025898_464323 -3715370_-3025898_464323 kind=PLANET orbit=12 radius=0.20180455276641018 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715378_-3025898_464329 kind=PLANET orbit=61 radius=0.8226351871084372 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715380_-3025901_464395 kind=GAS_GIANT orbit=389 radius=8.241293476987526 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715380_-3025901_464395 kind=MOON orbit=389 radius=0.47326136522912626 starId=-351188325 frame=false - body -3715368_-3025898_464323 -3715386_-3025895_464510 kind=GAS_GIANT orbit=1007 radius=3.104307714932146 starId=-351188325 frame=true - body -3715368_-3025898_464323 -3715386_-3025895_464510 kind=MOON orbit=1007 radius=0.2219279920272386 starId=-351188325 frame=false - body -3715368_-3025898_464323 -3715391_-3025898_464332 kind=PLANET orbit=130 radius=0.7953404597424061 starId=-351188325 frame=true - body -4100505_9119936_7447874 -4100505_9119936_7447874 kind=MOON orbit=0 radius=0.41094175801147914 starId=-1795081381 frame=false - body -4100505_9119936_7447874 -4100505_9119936_7447874 kind=ROGUE_PLANET orbit=0 radius=0.20249000003811363 starId=-1795081381 frame=true - body -628503_-2730462_8503404 -628503_-2730462_8503404 kind=MOON orbit=0 radius=0.8004976006437003 starId=-91454269 frame=false - body -628503_-2730462_8503404 -628503_-2730462_8503404 kind=ROGUE_PLANET orbit=0 radius=0.22475689697930112 starId=-91454269 frame=true - body -660880_-1054453_-895113 -660880_-1054453_-895113 kind=MOON orbit=0 radius=0.3744101559829215 starId=-1645210425 frame=false - body -660880_-1054453_-895113 -660880_-1054453_-895113 kind=MOON orbit=0 radius=0.4470232669285201 starId=-1645210425 frame=false - body -660880_-1054453_-895113 -660880_-1054453_-895113 kind=ROGUE_PLANET orbit=0 radius=0.5611689416997312 starId=-1645210425 frame=true - body 1268352_-472382_-2204417 1268259_-472378_-2204392 kind=ASTEROID_BELT orbit=515 radius=0.0 starId=-636287673 frame=true - body 1268352_-472382_-2204417 1268292_-472381_-2204414 kind=PLANET orbit=322 radius=0.6452755512774496 starId=-636287673 frame=true - body 1268352_-472382_-2204417 1268336_-472382_-2204397 kind=MOON orbit=139 radius=0.2470380566163046 starId=-636287673 frame=false - body 1268352_-472382_-2204417 1268336_-472382_-2204397 kind=PLANET orbit=139 radius=0.2165813118510709 starId=-636287673 frame=true - body 1268352_-472382_-2204417 1268344_-472382_-2204418 kind=GAS_GIANT orbit=42 radius=4.101154169118654 starId=-636287673 frame=true - body 1268352_-472382_-2204417 1268344_-472382_-2204418 kind=MOON orbit=42 radius=0.5585556554994643 starId=-636287673 frame=false - body 1268352_-472382_-2204417 1268350_-472382_-2204421 kind=ASTEROID_BELT orbit=23 radius=0.0 starId=-636287673 frame=true - body 1268352_-472382_-2204417 1268351_-472382_-2204415 kind=MOON orbit=11 radius=0.37683912902283956 starId=-636287673 frame=false - body 1268352_-472382_-2204417 1268351_-472382_-2204415 kind=MOON orbit=11 radius=0.5159283799812471 starId=-636287673 frame=false - body 1268352_-472382_-2204417 1268351_-472382_-2204415 kind=PLANET orbit=11 radius=0.5506753050718656 starId=-636287673 frame=true - body 1268352_-472382_-2204417 1268352_-472382_-2204417 kind=STAR orbit=0 radius=0.0 starId=-636287673 frame=true - body 1268352_-472382_-2204417 1268355_-472382_-2204418 kind=MOON orbit=17 radius=0.47086380378858367 starId=-636287673 frame=false - body 1268352_-472382_-2204417 1268355_-472382_-2204418 kind=MOON orbit=17 radius=0.6080086952896047 starId=-636287673 frame=false - body 1268352_-472382_-2204417 1268355_-472382_-2204418 kind=PLANET orbit=17 radius=1.203246167178117 starId=-636287673 frame=true - body 1315331_2596741_5713669 1315331_2596741_5713669 kind=MOON orbit=0 radius=1.0395123660391348 starId=-1517147849 frame=false - body 1315331_2596741_5713669 1315331_2596741_5713669 kind=MOON orbit=0 radius=1.8304825202249364 starId=-1517147849 frame=false - body 1315331_2596741_5713669 1315331_2596741_5713669 kind=ROGUE_PLANET orbit=0 radius=1.8909505397829076 starId=-1517147849 frame=true - body 1407594_9783198_-2449441 1407594_9783198_-2449441 kind=MOON orbit=0 radius=1.7372995652657248 starId=-1372461905 frame=false - body 1407594_9783198_-2449441 1407594_9783198_-2449441 kind=ROGUE_PLANET orbit=0 radius=0.47103298828651424 starId=-1372461905 frame=true - body 1673725_3372175_3108455 1673725_3372175_3108455 kind=ROGUE_PLANET orbit=0 radius=0.391336859643731 starId=-576770217 frame=true - body 1881534_-3011027_5110456 1881534_-3011027_5110456 kind=ROGUE_PLANET orbit=0 radius=2.359438373013696 starId=-1880844221 frame=true - body 2710691_-1047310_454879 2710691_-1047310_454879 kind=ROGUE_PLANET orbit=0 radius=6.180808030437285 starId=-1195104453 frame=true - body 3368241_5255982_7050837 3368241_5255982_7050837 kind=ROGUE_PLANET orbit=0 radius=0.3034346821187427 starId=-326311765 frame=true - body 4060263_3898666_-2963467 4060263_3898666_-2963467 kind=ROGUE_PLANET orbit=0 radius=0.42794466068253867 starId=-1719884493 frame=true - body 499372_6052057_2026466 499372_6052057_2026466 kind=ROGUE_PLANET orbit=0 radius=1.4507643648065525 starId=-1348043589 frame=true - body 5392052_6211062_-1062868 5391721_6211077_-1062265 kind=ASTEROID_BELT orbit=3678 radius=0.0 starId=-1954607381 frame=true - body 5392052_6211062_-1062868 5391982_6211059_-1062444 kind=PLANET orbit=2299 radius=0.27541638291159487 starId=-1954607381 frame=true - body 5392052_6211062_-1062868 5392042_6211062_-1062878 kind=MOON orbit=77 radius=0.24439099491676247 starId=-1954607381 frame=false - body 5392052_6211062_-1062868 5392042_6211062_-1062878 kind=MOON orbit=77 radius=0.3102458262031239 starId=-1954607381 frame=false - body 5392052_6211062_-1062868 5392042_6211062_-1062878 kind=PLANET orbit=77 radius=2.3911871875797917 starId=-1954607381 frame=true - body 5392052_6211062_-1062868 5392052_6211062_-1062868 kind=STAR orbit=0 radius=0.0 starId=-1954607381 frame=true - body 5392052_6211062_-1062868 5392095_6211060_-1062881 kind=PLANET orbit=240 radius=0.7707202888918347 starId=-1954607381 frame=true - body 5504536_9800924_3923283 5504515_9800927_3923212 kind=GAS_GIANT orbit=398 radius=3.171788138456746 starId=-1100599953 frame=true - body 5504536_9800924_3923283 5504515_9800927_3923212 kind=MOON orbit=398 radius=0.22933661208386558 starId=-1100599953 frame=false - body 5504536_9800924_3923283 5504515_9800927_3923212 kind=MOON orbit=398 radius=0.31451081886642485 starId=-1100599953 frame=false - body 5504536_9800924_3923283 5504515_9800927_3923212 kind=MOON orbit=398 radius=0.35097569265251594 starId=-1100599953 frame=false - body 5504536_9800924_3923283 5504536_9800924_3923283 kind=STAR orbit=0 radius=0.0 starId=-1100599953 frame=true - body 5504536_9800924_3923283 5504537_9800924_3923282 kind=MOON orbit=7 radius=0.40009045843515056 starId=-1100599953 frame=false - body 5504536_9800924_3923283 5504537_9800924_3923282 kind=MOON orbit=7 radius=0.7146264002883986 starId=-1100599953 frame=false - body 5504536_9800924_3923283 5504537_9800924_3923282 kind=PLANET orbit=7 radius=1.7986200380985606 starId=-1100599953 frame=true - body 5504536_9800924_3923283 5504538_9800924_3923276 kind=MOON orbit=40 radius=0.3179143781697107 starId=-1100599953 frame=false - body 5504536_9800924_3923283 5504538_9800924_3923276 kind=MOON orbit=40 radius=0.6523342704190511 starId=-1100599953 frame=false - body 5504536_9800924_3923283 5504538_9800924_3923276 kind=PLANET orbit=40 radius=0.4069949825213218 starId=-1100599953 frame=true - body 5504536_9800924_3923283 5504548_9800924_3923322 kind=ASTEROID_BELT orbit=221 radius=0.0 starId=-1100599953 frame=true - body 5504536_9800924_3923283 5504552_9800925_3923300 kind=PLANET orbit=126 radius=0.35701423058093684 starId=-1100599953 frame=true - body 5504536_9800924_3923283 5504644_9800924_3923234 kind=ASTEROID_BELT orbit=636 radius=0.0 starId=-1100599953 frame=true - body 6282550_3920081_-4327174 6282550_3920081_-4327174 kind=ROGUE_PLANET orbit=0 radius=0.20476677664862225 starId=-1926330197 frame=true - body 7614190_8929461_6983314 7614190_8929461_6983314 kind=ROGUE_PLANET orbit=0 radius=0.3244906791992829 starId=-397610089 frame=true - body 7821430_3964300_8607040 7821430_3964300_8607040 kind=MOON orbit=0 radius=0.9204718241145495 starId=-979945297 frame=false - body 7821430_3964300_8607040 7821430_3964300_8607040 kind=ROGUE_PLANET orbit=0 radius=0.5873505561269114 starId=-979945297 frame=true - body 8556534_-4759325_7276386 8556534_-4759325_7276386 kind=MOON orbit=0 radius=0.21529441503824975 starId=-1513594621 frame=false - body 8556534_-4759325_7276386 8556534_-4759325_7276386 kind=MOON orbit=0 radius=0.3315137311013633 starId=-1513594621 frame=false - body 8556534_-4759325_7276386 8556534_-4759325_7276386 kind=ROGUE_PLANET orbit=0 radius=0.25120702885343593 starId=-1513594621 frame=true - body 8869249_4721320_3066528 8869249_4721320_3066528 kind=MOON orbit=0 radius=2.260316585289282 starId=-893649661 frame=false - body 8869249_4721320_3066528 8869249_4721320_3066528 kind=ROGUE_PLANET orbit=0 radius=1.0153541787850344 starId=-893649661 frame=true - body 9342137_-1318710_-2154197 9342137_-1318710_-2154197 kind=MOON orbit=0 radius=0.7561660356829851 starId=-364163833 frame=false - body 9342137_-1318710_-2154197 9342137_-1318710_-2154197 kind=MOON orbit=0 radius=1.6234315866951836 starId=-364163833 frame=false - body 9342137_-1318710_-2154197 9342137_-1318710_-2154197 kind=ROGUE_PLANET orbit=0 radius=0.6714008663449185 starId=-364163833 frame=true - body 9459338_-4115184_3189023 9459338_-4115184_3189023 kind=ROGUE_PLANET orbit=0 radius=0.9513578971220109 starId=-40991845 frame=true - derived -1440306_4321771_-1644504 -1440306_4321771_-1644504 type=ice mass=10.377861862102423 radius=1.858589898702767 gravity=300 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=19065 metallicity=0.6057706896698947 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1487091_5789006_3108422 -1487091_5789006_3108422 type=barren mass=0.45371319611386035 radius=0.7622212449935264 gravity=78 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=40969 metallicity=1.1313068142223444 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2182872_2271086_929819 -2182872_2271086_929819 type=ice mass=4.930558943669398 radius=1.5594868722814699 gravity=203 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=14486 metallicity=0.8780061621167181 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2532019_1322888_8152935 -2532019_1322888_8152935 type=superearth mass=13.341765256018288 radius=1.95774137431041 gravity=348 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=42915 metallicity=1.463175358049182 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2648150_9876469_-4697214 -2648150_9876469_-4697214 type=superearth mass=11.122553864105102 radius=1.8158165163169306 gravity=337 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=15076 metallicity=1.0301874894482133 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715067_-3025894_464264 type=ice mass=1.9202967701603253 radius=1.1310343685083477 gravity=150 pressure=1600 tempK=85 oxygen=false locked=false rings=false rotation=13803 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715248_-3025896_464384 type=ice mass=29.938849817208148 radius=2.3765942402912463 gravity=400 pressure=1600 tempK=129 oxygen=false locked=false rings=false rotation=57034 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715284_-3025898_464806 type=ice mass=1.9117522949109045 radius=1.1230195340795335 gravity=152 pressure=1600 tempK=67 oxygen=false locked=false rings=false rotation=13796 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715331_-3025899_464308 type=exotic mass=2.1019781855337327 radius=1.2170316663420997 gravity=142 pressure=1600 tempK=271 oxygen=false locked=false rings=false rotation=17356 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715334_-3025900_464348 type=ice mass=0.010144834480563766 radius=0.28266309604664974 gravity=13 pressure=0 tempK=103 oxygen=false locked=false rings=false rotation=39841 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715355_-3025899_464334 type=superearth mass=14.907317191154986 radius=2.131531437678731 gravity=328 pressure=1600 tempK=420 oxygen=false locked=false rings=false rotation=29565 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715364_-3025898_464319 type=greenhouse mass=19.769566824621222 radius=2.2697636812476105 gravity=384 pressure=1600 tempK=554 oxygen=false locked=true rings=false rotation=77807 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715366_-3025898_464319 type=desert mass=0.005334838248948286 radius=0.23651511772733647 gravity=10 pressure=0 tempK=378 oxygen=false locked=true rings=false rotation=39327 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715368_-3025898_464323 type=lava mass=0.0033230152860885755 radius=0.2023556097717985 gravity=8 pressure=0 tempK=1889 oxygen=false locked=true rings=false rotation=21593 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715370_-3025898_464323 type=barren mass=0.0027917778218846064 radius=0.20180455276641018 gravity=7 pressure=0 tempK=542 oxygen=false locked=true rings=false rotation=6357 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715378_-3025898_464329 type=ice mass=0.37963888245778626 radius=0.8226351871084372 gravity=56 pressure=31 tempK=197 oxygen=false locked=false rings=false rotation=13777 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715380_-3025901_464395 type=gasgiant mass=163.68667159513973 radius=8.241293476987526 gravity=241 pressure=1600 tempK=186 oxygen=false locked=false rings=false rotation=7948 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715386_-3025895_464510 type=icegiant mass=17.327779061480165 radius=3.104307714932146 gravity=180 pressure=1600 tempK=115 oxygen=false locked=false rings=true rotation=6186 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3715368_-3025898_464323 -3715391_-3025898_464332 type=ice mass=0.3974007358220052 radius=0.7953404597424061 gravity=63 pressure=156 tempK=170 oxygen=false locked=false rings=false rotation=9797 metallicity=0.5808359949879165 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4100505_9119936_7447874 -4100505_9119936_7447874 type=ice mass=0.002925677165718173 radius=0.20249000003811363 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=53423 metallicity=1.112540504699406 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -628503_-2730462_8503404 -628503_-2730462_8503404 type=barren mass=0.0035874996009393812 radius=0.22475689697930112 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=91158 metallicity=0.49615603963499344 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -660880_-1054453_-895113 -660880_-1054453_-895113 type=barren mass=0.09756822926774975 radius=0.5611689416997312 gravity=31 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=41164 metallicity=1.2157740957062755 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1268352_-472382_-2204417 1268259_-472378_-2204392 type=superearth mass=7.927755523878962 radius=1.6586029986911508 gravity=288 pressure=1600 tempK=85 oxygen=false locked=false rings=false rotation=48149 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1268352_-472382_-2204417 1268292_-472381_-2204414 type=ice mass=0.1978383843924024 radius=0.6452755512774496 gravity=48 pressure=122 tempK=49 oxygen=false locked=false rings=false rotation=41315 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1268352_-472382_-2204417 1268336_-472382_-2204397 type=ice mass=0.00345957043996053 radius=0.2165813118510709 gravity=7 pressure=0 tempK=63 oxygen=false locked=false rings=false rotation=25031 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1268352_-472382_-2204417 1268344_-472382_-2204418 type=gasgiant mass=32.8781947501483 radius=4.101154169118654 gravity=195 pressure=1600 tempK=274 oxygen=false locked=false rings=true rotation=6488 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1268352_-472382_-2204417 1268350_-472382_-2204421 type=gasgiant mass=52.838738780413046 radius=5.040697686360118 gravity=208 pressure=1600 tempK=371 oxygen=false locked=false rings=false rotation=9002 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1268352_-472382_-2204417 1268351_-472382_-2204415 type=desert mass=0.1297786978767138 radius=0.5506753050718656 gravity=43 pressure=6 tempK=259 oxygen=false locked=true rings=false rotation=80284 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1268352_-472382_-2204417 1268352_-472382_-2204417 type=superearth mass=4.447393609080987 radius=1.4777327469738462 gravity=204 pressure=44 tempK=860 oxygen=false locked=true rings=false rotation=17966 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1268352_-472382_-2204417 1268355_-472382_-2204418 type=greenhouse mass=1.9643183536374995 radius=1.203246167178117 gravity=136 pressure=1600 tempK=363 oxygen=false locked=true rings=false rotation=8535 metallicity=1.098004351109636 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1315331_2596741_5713669 1315331_2596741_5713669 type=ice mass=12.934325599371203 radius=1.8909505397829076 gravity=362 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=13371 metallicity=0.8349568742803939 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1407594_9783198_-2449441 1407594_9783198_-2449441 type=barren mass=0.058516468193337154 radius=0.47103298828651424 gravity=26 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=18840 metallicity=0.40428506216328813 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1673725_3372175_3108455 1673725_3372175_3108455 type=ice mass=0.03112067865715841 radius=0.391336859643731 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=63128 metallicity=1.2085371618398715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1881534_-3011027_5110456 1881534_-3011027_5110456 type=superearth mass=29.429310866728557 radius=2.359438373013696 gravity=400 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=12431 metallicity=1.4280539746788947 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2710691_-1047310_454879 2710691_-1047310_454879 type=gasgiant mass=84.45551815291512 radius=6.180808030437285 gravity=221 pressure=1600 tempK=43 oxygen=false locked=false rings=true rotation=6678 metallicity=0.84226801458673 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3368241_5255982_7050837 3368241_5255982_7050837 type=ice mass=0.013454795033272725 radius=0.3034346821187427 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=36979 metallicity=0.48929816684250027 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4060263_3898666_-2963467 4060263_3898666_-2963467 type=barren mass=0.05147967174733844 radius=0.42794466068253867 gravity=28 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=9260 metallicity=1.5423537513918921 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 499372_6052057_2026466 499372_6052057_2026466 type=ice mass=4.644453342051514 radius=1.4507643648065525 gravity=221 pressure=0 tempK=43 oxygen=false locked=false rings=false rotation=19002 metallicity=0.8983108650169582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5392052_6211062_-1062868 5391721_6211077_-1062265 type=superearth mass=8.681226927094112 radius=1.7870233158155744 gravity=272 pressure=1600 tempK=105 oxygen=false locked=false rings=false rotation=7206 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5392052_6211062_-1062868 5391982_6211059_-1062444 type=barren mass=0.009913430355922298 radius=0.27541638291159487 gravity=13 pressure=1 tempK=62 oxygen=false locked=false rings=false rotation=54635 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5392052_6211062_-1062868 5392042_6211062_-1062878 type=superearth mass=22.348141890617914 radius=2.3911871875797917 gravity=391 pressure=1600 tempK=726 oxygen=false locked=false rings=false rotation=28856 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5392052_6211062_-1062868 5392052_6211062_-1062868 type=lava mass=2.6109467287345822 radius=1.2905541091838826 gravity=157 pressure=1 tempK=3018 oxygen=false locked=true rings=false rotation=25951 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5392052_6211062_-1062868 5392095_6211060_-1062881 type=ice mass=0.30143691962074926 radius=0.7707202888918347 gravity=51 pressure=51 tempK=159 oxygen=false locked=false rings=false rotation=77653 metallicity=0.8843419292911162 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5504536_9800924_3923283 5504515_9800927_3923212 type=gasgiant mass=18.206377216773813 radius=3.171788138456746 gravity=181 pressure=1600 tempK=96 oxygen=false locked=false rings=false rotation=4863 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5504536_9800924_3923283 5504536_9800924_3923283 type=barren mass=0.08859859371409555 radius=0.4914168748804031 gravity=37 pressure=0 tempK=985 oxygen=false locked=true rings=false rotation=14638 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5504536_9800924_3923283 5504537_9800924_3923282 type=lava mass=10.705415494759238 radius=1.7986200380985606 gravity=331 pressure=1600 tempK=842 oxygen=false locked=true rings=false rotation=9773 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5504536_9800924_3923283 5504538_9800924_3923276 type=barren mass=0.030448746587449775 radius=0.4069949825213218 gravity=18 pressure=1 tempK=155 oxygen=false locked=true rings=false rotation=40600 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5504536_9800924_3923283 5504548_9800924_3923322 type=icegiant mass=202.20009831621877 radius=9.034300451641911 gravity=248 pressure=1600 tempK=129 oxygen=false locked=false rings=true rotation=5217 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5504536_9800924_3923283 5504552_9800925_3923300 type=ice mass=0.022887226912599735 radius=0.35701423058093684 gravity=18 pressure=1 tempK=72 oxygen=false locked=false rings=false rotation=23388 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5504536_9800924_3923283 5504644_9800924_3923234 type=barren mass=0.034213554407382614 radius=0.38774183430029346 gravity=23 pressure=5 tempK=39 oxygen=false locked=false rings=false rotation=66550 metallicity=1.1815528872614258 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6282550_3920081_-4327174 6282550_3920081_-4327174 type=barren mass=0.002877051787226516 radius=0.20476677664862225 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=30118 metallicity=0.6453043137367989 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7614190_8929461_6983314 7614190_8929461_6983314 type=barren mass=0.012124284240218575 radius=0.3244906791992829 gravity=12 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=78290 metallicity=1.386929789193736 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7821430_3964300_8607040 7821430_3964300_8607040 type=barren mass=0.13119556478427508 radius=0.5873505561269114 gravity=38 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=47532 metallicity=0.6269401359278104 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8556534_-4759325_7276386 8556534_-4759325_7276386 type=barren mass=0.006142163713278197 radius=0.25120702885343593 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=76639 metallicity=1.0565819982317461 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8869249_4721320_3066528 8869249_4721320_3066528 type=barren mass=0.845801395899521 radius=1.0153541787850344 gravity=82 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=25248 metallicity=1.2929327969937456 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9342137_-1318710_-2154197 9342137_-1318710_-2154197 type=barren mass=0.21029229819468406 radius=0.6714008663449185 gravity=47 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=19850 metallicity=0.8363712659150969 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9459338_-4115184_3189023 9459338_-4115184_3189023 type=ice mass=0.7470096901599058 radius=0.9513578971220109 gravity=83 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=47356 metallicity=0.601570911220727 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1440306_4321771_-1644504 id=-224544461 kind=ROGUE_PLANET name=PGR--5002361.0.-5002361 starless - system -1487091_5789006_3108422 id=-1737974265 kind=ROGUE_PLANET name=PGR--5002361.5002361.0 starless - system -2182872_2271086_929819 id=-852504905 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless - system -2532019_1322888_8152935 id=-1880339145 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless - system -2648150_9876469_-4697214 id=-337840993 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless - system -3715368_-3025898_464323 id=-351188325 kind=STAR name=PGS--5002361.-5002361.0 starTemp=70 starSize=0.9819875955581665 - system -4100505_9119936_7447874 id=-1795081381 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless - system -628503_-2730462_8503404 id=-91454269 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless - system -660880_-1054453_-895113 id=-1645210425 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless - system 1268352_-472382_-2204417 id=-636287673 kind=STAR name=PGS-0.-5002361.-5002361 starTemp=40 starSize=0.7072588205337524 - system 1315331_2596741_5713669 id=-1517147849 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless - system 1407594_9783198_-2449441 id=-1372461905 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless - system 1673725_3372175_3108455 id=-576770217 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 1881534_-3011027_5110456 id=-1880844221 kind=ROGUE_PLANET name=PGR-0.-5002361.5002361 starless - system 2710691_-1047310_454879 id=-1195104453 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless - system 3368241_5255982_7050837 id=-326311765 kind=ROGUE_PLANET name=PGR-0.5002361.5002361 starless - system 4060263_3898666_-2963467 id=-1719884493 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless - system 499372_6052057_2026466 id=-1348043589 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless - system 5392052_6211062_-1062868 id=-1954607381 kind=STAR name=PGS-5002361.5002361.-5002361 starTemp=100 starSize=1.2276172637939453 - system 5504536_9800924_3923283 id=-1100599953 kind=STAR name=PGS-5002361.5002361.0 starTemp=40 starSize=0.827074408531189 - system 6282550_3920081_-4327174 id=-1926330197 kind=ROGUE_PLANET name=PGR-5002361.0.-5002361 starless - system 7614190_8929461_6983314 id=-397610089 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless - system 7821430_3964300_8607040 id=-979945297 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless - system 8556534_-4759325_7276386 id=-1513594621 kind=ROGUE_PLANET name=PGR-5002361.-5002361.5002361 starless - system 8869249_4721320_3066528 id=-893649661 kind=ROGUE_PLANET name=PGR-5002361.0.0 starless - system 9342137_-1318710_-2154197 id=-364163833 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless - system 9459338_-4115184_3189023 id=-40991845 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless + body -1354226_-775980_5896155 -1354226_-775980_5896155 kind=ROGUE_PLANET orbit=0 radius=0.2095823280782168 starId=-245847961 frame=true at=0,0,0 + body -1786692_-493284_1495624 -1786692_-493284_1495624 kind=MOON orbit=0 radius=0.7693291786735446 starId=-1420304977 frame=false at=-36403,0,-3793 + body -1786692_-493284_1495624 -1786692_-493284_1495624 kind=ROGUE_PLANET orbit=0 radius=0.20049653427640265 starId=-1420304977 frame=true at=0,0,0 + body -2263975_2500210_-512704 -2263975_2500210_-512704 kind=ROGUE_PLANET orbit=0 radius=2.3061179305736363 starId=-544380065 frame=true at=0,0,0 + body -2453677_750464_5429323 -2453677_750464_5429323 kind=ROGUE_PLANET orbit=0 radius=0.20615669038861043 starId=-1103945881 frame=true at=0,0,0 + body -2492763_-2673435_-1919668 -2492763_-2673435_-1919668 kind=ROGUE_PLANET orbit=0 radius=0.9387143903262674 starId=-115633053 frame=true at=0,0,0 + body -2783529_5420979_-114688 -2783529_5420979_-114688 kind=ROGUE_PLANET orbit=0 radius=2.0905623514345244 starId=-301293885 frame=true at=0,0,0 + body -3019098_4194427_955152 -3019098_4194427_955152 kind=MOON orbit=0 radius=0.6449988370706554 starId=-203567605 frame=false at=56478,0,20838 + body -3019098_4194427_955152 -3019098_4194427_955152 kind=ROGUE_PLANET orbit=0 radius=0.20200912568214305 starId=-203567605 frame=true at=0,0,0 + body -917361_6648687_4231175 -917361_6648687_4231175 kind=MOON orbit=0 radius=0.3356662908767457 starId=-1415803897 frame=false at=213478,0,188214 + body -917361_6648687_4231175 -917361_6648687_4231175 kind=ROGUE_PLANET orbit=0 radius=2.3937523821350277 starId=-1415803897 frame=true at=0,0,0 + body -987441_2574102_354383 -987441_2574102_354383 kind=MOON orbit=0 radius=1.658573863970683 starId=-1653738961 frame=false at=45197,0,-40966 + body -987441_2574102_354383 -987441_2574102_354383 kind=ROGUE_PLANET orbit=0 radius=0.4498780048941231 starId=-1653738961 frame=true at=0,0,0 + body 1192693_4180823_6715391 1192693_4180823_6715391 kind=MOON orbit=0 radius=0.20399904677206301 starId=-1464588085 frame=false at=16001,0,-45679 + body 1192693_4180823_6715391 1192693_4180823_6715391 kind=ROGUE_PLANET orbit=0 radius=0.23945296389324872 starId=-1464588085 frame=true at=0,0,0 + body 137508_498291_5548492 137508_498291_5548492 kind=ROGUE_PLANET orbit=0 radius=1.2332711328278239 starId=-1629913369 frame=true at=0,0,0 + body 1926001_-2868405_6450292 1926001_-2868405_6450292 kind=ROGUE_PLANET orbit=0 radius=0.6672128988717654 starId=-1724907213 frame=true at=0,0,0 + body 2329541_4966897_-1325776 2329541_4966897_-1325776 kind=STAR orbit=0 radius=0.0 starId=-1372746905 frame=true at=0,0,0 + body 2329541_4966897_-1325776 2329541_4966898_-1325750 kind=MOON orbit=137 radius=0.3693396668206692 starId=-1372746905 frame=false at=-22479,0,-39444 + body 2329541_4966897_-1325776 2329541_4966898_-1325750 kind=MOON orbit=137 radius=0.4330651757074263 starId=-1372746905 frame=false at=-32862,0,-23154 + body 2329541_4966897_-1325776 2329541_4966898_-1325750 kind=PLANET orbit=137 radius=0.4327238813330402 starId=-1372746905 frame=true at=0,0,0 + body 2329541_4966897_-1325776 2329543_4966897_-1325771 kind=MOON orbit=27 radius=0.49281179784880225 starId=-1372746905 frame=false at=119888,0,-105352 + body 2329541_4966897_-1325776 2329543_4966897_-1325771 kind=PLANET orbit=27 radius=0.5664330555906398 starId=-1372746905 frame=true at=0,0,0 + body 2329541_4966897_-1325776 2329543_4966897_-1325774 kind=MOON orbit=14 radius=0.5063668041753813 starId=-1372746905 frame=false at=133101,0,-98527 + body 2329541_4966897_-1325776 2329543_4966897_-1325774 kind=PLANET orbit=14 radius=0.5911736961048453 starId=-1372746905 frame=true at=0,0,0 + body 2329541_4966897_-1325776 2329577_4966896_-1325777 kind=ASTEROID_BELT orbit=192 radius=0.0 starId=-1372746905 frame=true at=0,0,0 + body 2329541_4966897_-1325776 2329605_4966897_-1325785 kind=GAS_GIANT orbit=346 radius=10.17528264925558 starId=-1372746905 frame=true at=0,0,0 + body 2329541_4966897_-1325776 2329605_4966897_-1325785 kind=MOON orbit=346 radius=0.23621311427066893 starId=-1372746905 frame=false at=-2302671,0,-1848760 + body 2329541_4966897_-1325776 2329644_4966893_-1325768 kind=ASTEROID_BELT orbit=553 radius=0.0 starId=-1372746905 frame=true at=0,0,0 + body 2889434_-3209715_-1090932 2883106_-3209715_-1087241 kind=STAR orbit=39178 radius=106.17007929861546 starId=-1616632874 frame=true at=0,0,0 + body 2889434_-3209715_-1090932 2889367_-3209711_-1090865 kind=GAS_GIANT orbit=508 radius=7.359168562793266 starId=-1616632873 frame=true at=0,0,0 + body 2889434_-3209715_-1090932 2889422_-3209715_-1090925 kind=ASTEROID_BELT orbit=72 radius=0.0 starId=-1616632873 frame=true at=0,0,0 + body 2889434_-3209715_-1090932 2889434_-3209715_-1090925 kind=MOON orbit=35 radius=0.20221583652227892 starId=-1616632873 frame=false at=-123674,0,327635 + body 2889434_-3209715_-1090932 2889434_-3209715_-1090925 kind=MOON orbit=35 radius=0.5378447126285323 starId=-1616632873 frame=false at=416020,0,576584 + body 2889434_-3209715_-1090932 2889434_-3209715_-1090925 kind=PLANET orbit=35 radius=2.3898701900773904 starId=-1616632873 frame=true at=0,0,0 + body 2889434_-3209715_-1090932 2889434_-3209715_-1090930 kind=MOON orbit=10 radius=0.21415529303721711 starId=-1616632873 frame=false at=-19511,0,25111 + body 2889434_-3209715_-1090932 2889434_-3209715_-1090930 kind=MOON orbit=10 radius=0.5605312080448812 starId=-1616632873 frame=false at=7333,0,12856 + body 2889434_-3209715_-1090932 2889434_-3209715_-1090930 kind=PLANET orbit=10 radius=0.20001270394003767 starId=-1616632873 frame=true at=0,0,0 + body 2889434_-3209715_-1090932 2889434_-3209715_-1090932 kind=STAR orbit=0 radius=0.0 starId=-1616632873 frame=true at=0,0,0 + body 2889434_-3209715_-1090932 2889455_-3209716_-1090920 kind=GAS_GIANT orbit=131 radius=6.384029999980021 starId=-1616632873 frame=true at=0,0,0 + body 2889434_-3209715_-1090932 2889455_-3209716_-1090920 kind=MOON orbit=131 radius=0.20006308834778394 starId=-1616632873 frame=false at=277681,0,-1682236 + body 2889434_-3209715_-1090932 2889455_-3209716_-1090920 kind=MOON orbit=131 radius=0.39075281887512414 starId=-1616632873 frame=false at=1235464,0,-354073 + body 2889434_-3209715_-1090932 2889556_-3209712_-1091022 kind=ASTEROID_BELT orbit=812 radius=0.0 starId=-1616632873 frame=true at=0,0,0 + body 3182346_-1200494_1223840 3182346_-1200494_1223840 kind=MOON orbit=0 radius=0.7525009834429903 starId=-1628626657 frame=false at=-74042,0,-192877 + body 3182346_-1200494_1223840 3182346_-1200494_1223840 kind=ROGUE_PLANET orbit=0 radius=0.7273830659106353 starId=-1628626657 frame=true at=0,0,0 + body 3260852_6822578_1746102 3260852_6822578_1746102 kind=ROGUE_PLANET orbit=0 radius=1.2795628404660984 starId=-24266345 frame=true at=0,0,0 + body 3723419_2156869_-1335565 3723396_2156869_-1335603 kind=GAS_GIANT orbit=239 radius=7.725594531511088 starId=-392697649 frame=true at=0,0,0 + body 3723419_2156869_-1335565 3723396_2156869_-1335603 kind=MOON orbit=239 radius=0.20291586860664781 starId=-392697649 frame=false at=-1416202,0,597278 + body 3723419_2156869_-1335565 3723396_2156869_-1335603 kind=MOON orbit=239 radius=0.7278912456238515 starId=-392697649 frame=false at=1381989,0,-1257091 + body 3723419_2156869_-1335565 3723397_2156870_-1335553 kind=ASTEROID_BELT orbit=132 radius=0.0 starId=-392697649 frame=true at=0,0,0 + body 3723419_2156869_-1335565 3723418_2156868_-1335551 kind=PLANET orbit=73 radius=1.633148040692367 starId=-392697649 frame=true at=0,0,0 + body 3723419_2156869_-1335565 3723419_2156869_-1335565 kind=STAR orbit=0 radius=0.0 starId=-392697649 frame=true at=0,0,0 + body 3723419_2156869_-1335565 3723422_2156869_-1335564 kind=MOON orbit=19 radius=0.34710916896983884 starId=-392697649 frame=false at=-12446,0,43661 + body 3723419_2156869_-1335565 3723422_2156869_-1335564 kind=PLANET orbit=19 radius=0.20888798108361883 starId=-392697649 frame=true at=0,0,0 + body 3723419_2156869_-1335565 3723441_2156872_-1335633 kind=ASTEROID_BELT orbit=382 radius=0.0 starId=-392697649 frame=true at=0,0,0 + body 3822552_4765867_-2982992 3822552_4765867_-2982992 kind=ROGUE_PLANET orbit=0 radius=0.7075016622871881 starId=-646721517 frame=true at=0,0,0 + body 3927952_4810457_3631655 3924557_4810385_3631323 kind=GAS_GIANT orbit=18248 radius=7.532755154412924 starId=-771612361 frame=true at=0,0,0 + body 3927952_4810457_3631655 3924557_4810385_3631323 kind=MOON orbit=18248 radius=0.2398248770474635 starId=-771612361 frame=false at=773716,0,-992185 + body 3927952_4810457_3631655 3924557_4810385_3631323 kind=MOON orbit=18248 radius=0.3250924220967756 starId=-771612361 frame=false at=-1724671,0,62410 + body 3927952_4810457_3631655 3926914_4810506_3631484 kind=PLANET orbit=5631 radius=1.4188668287815471 starId=-771612361 frame=true at=0,0,0 + body 3927952_4810457_3631655 3927727_4810461_3631449 kind=GAS_GIANT orbit=1632 radius=4.830052167417934 starId=-771612361 frame=true at=0,0,0 + body 3927952_4810457_3631655 3927882_4810463_3631809 kind=ASTEROID_BELT orbit=906 radius=0.0 starId=-771612361 frame=true at=0,0,0 + body 3927952_4810457_3631655 3927952_4810457_3631655 kind=STAR orbit=0 radius=0.0 starId=-771612361 frame=true at=0,0,0 + body 3927952_4810457_3631655 3927997_4810459_3631662 kind=MOON orbit=241 radius=0.49560078394410945 starId=-771612361 frame=false at=-95660,0,-587665 + body 3927952_4810457_3631655 3927997_4810459_3631662 kind=PLANET orbit=241 radius=2.078438494405952 starId=-771612361 frame=true at=0,0,0 + body 3927952_4810457_3631655 3933209_4810411_3630182 kind=ASTEROID_BELT orbit=29196 radius=0.0 starId=-771612361 frame=true at=0,0,0 + body 3991853_-3409879_1695966 3991812_-3409882_1696009 kind=PLANET orbit=320 radius=0.2000049072546915 starId=-532811557 frame=true at=0,0,0 + body 3991853_-3409879_1695966 3991834_-3409881_1695997 kind=PLANET orbit=194 radius=2.428323573235008 starId=-532811557 frame=true at=0,0,0 + body 3991853_-3409879_1695966 3991853_-3409879_1695966 kind=STAR orbit=0 radius=0.0 starId=-532811557 frame=true at=0,0,0 + body 3991853_-3409879_1695966 3991853_-3409879_1695970 kind=ASTEROID_BELT orbit=20 radius=0.0 starId=-532811557 frame=true at=0,0,0 + body 3991853_-3409879_1695966 3991854_-3409879_1695962 kind=PLANET orbit=20 radius=0.22033987090436358 starId=-532811557 frame=true at=0,0,0 + body 3991853_-3409879_1695966 3991854_-3409879_1695966 kind=PLANET orbit=8 radius=0.22662800708790426 starId=-532811557 frame=true at=0,0,0 + body 3991853_-3409879_1695966 3991859_-3409879_1695970 kind=GAS_GIANT orbit=36 radius=9.047143938187649 starId=-532811557 frame=true at=0,0,0 + body 3991853_-3409879_1695966 3991868_-3409878_1695958 kind=MOON orbit=93 radius=0.22995041668892738 starId=-532811557 frame=false at=174171,0,47757 + body 3991853_-3409879_1695966 3991868_-3409878_1695958 kind=MOON orbit=93 radius=0.6330754458586816 starId=-532811557 frame=false at=-249338,0,168616 + body 3991853_-3409879_1695966 3991868_-3409878_1695958 kind=PLANET orbit=93 radius=1.2416552219403456 starId=-532811557 frame=true at=0,0,0 + body 3991853_-3409879_1695966 3991928_-3409882_1696026 kind=ASTEROID_BELT orbit=512 radius=0.0 starId=-532811557 frame=true at=0,0,0 + body 4713087_-1117360_-531483 4713087_-1117360_-531483 kind=ROGUE_PLANET orbit=0 radius=0.5492908503901086 starId=-1273646913 frame=true at=0,0,0 + body 5671190_876631_2238047 5671190_876631_2238047 kind=MOON orbit=0 radius=1.8347587972498054 starId=-1812954729 frame=false at=-59469,0,53512 + body 5671190_876631_2238047 5671190_876631_2238047 kind=ROGUE_PLANET orbit=0 radius=0.3623353932076434 starId=-1812954729 frame=true at=0,0,0 + body 6440405_-2011426_5750662 6440405_-2011426_5750662 kind=ROGUE_PLANET orbit=0 radius=2.1711386375337 starId=-929483845 frame=true at=0,0,0 + body 6464859_1421762_4096630 6464859_1421762_4096630 kind=MOON orbit=0 radius=0.739675313296466 starId=-1373082229 frame=false at=200198,0,-248615 + body 6464859_1421762_4096630 6464859_1421762_4096630 kind=ROGUE_PLANET orbit=0 radius=1.3688524064028083 starId=-1373082229 frame=true at=0,0,0 + body 662972_1432839_1639281 662972_1432839_1639281 kind=MOON orbit=0 radius=0.30956397773965205 starId=-576770217 frame=false at=70016,0,240204 + body 662972_1432839_1639281 662972_1432839_1639281 kind=ROGUE_PLANET orbit=0 radius=1.8066540452223296 starId=-576770217 frame=true at=0,0,0 + body 6924773_4236388_1837031 6924773_4236388_1837031 kind=ROGUE_PLANET orbit=0 radius=0.25767849632181145 starId=-1211781741 frame=true at=0,0,0 + body 793862_2575944_-859446 793862_2575944_-859446 kind=MOON orbit=0 radius=1.0287217786888536 starId=-508002589 frame=false at=88065,0,-17567 + body 793862_2575944_-859446 793862_2575944_-859446 kind=ROGUE_PLANET orbit=0 radius=0.5129142760947367 starId=-508002589 frame=true at=0,0,0 + derived -1354226_-775980_5896155 -1354226_-775980_5896155 type=ice mass=0.0031285574176620656 radius=0.2095823280782168 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=9265 metallicity=1.5347243409224713 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1786692_-493284_1495624 -1786692_-493284_1495624 type=barren mass=0.0023689048914081463 radius=0.20049653427640265 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=10746 metallicity=0.8821677094649482 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2263975_2500210_-512704 -2263975_2500210_-512704 type=ice mass=26.29840821160823 radius=2.3061179305736363 gravity=400 pressure=0 tempK=52 oxygen=false locked=false rings=false rotation=6300 metallicity=0.6192761496322858 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2453677_750464_5429323 -2453677_750464_5429323 type=ice mass=0.002322737197980196 radius=0.20615669038861043 gravity=5 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=28078 metallicity=0.9579572043963579 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2492763_-2673435_-1919668 -2492763_-2673435_-1919668 type=ice mass=0.8524265034311743 radius=0.9387143903262674 gravity=97 pressure=0 tempK=35 oxygen=false locked=false rings=true rotation=44275 metallicity=0.9064800766555258 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2783529_5420979_-114688 -2783529_5420979_-114688 type=ice mass=16.178098163884066 radius=2.0905623514345244 gravity=370 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=11736 metallicity=0.782820294838597 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3019098_4194427_955152 -3019098_4194427_955152 type=barren mass=0.0025857474895548105 radius=0.20200912568214305 gravity=6 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=34059 metallicity=0.35261620615877204 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -917361_6648687_4231175 -917361_6648687_4231175 type=ice mass=21.61810436895705 radius=2.3937523821350277 gravity=377 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=16371 metallicity=0.41798816435242464 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -987441_2574102_354383 -987441_2574102_354383 type=barren mass=0.04936332087010038 radius=0.4498780048941231 gravity=24 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=29600 metallicity=0.879350259899443 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1192693_4180823_6715391 1192693_4180823_6715391 type=ice mass=0.0057940503067513555 radius=0.23945296389324872 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=76651 metallicity=1.0762488182169192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 137508_498291_5548492 137508_498291_5548492 type=superearth mass=2.64578834872378 radius=1.2332711328278239 gravity=174 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=68817 metallicity=0.6373928080636878 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1926001_-2868405_6450292 1926001_-2868405_6450292 type=barren mass=0.27026928160409147 radius=0.6672128988717654 gravity=61 pressure=0 tempK=31 oxygen=false locked=false rings=false rotation=26219 metallicity=1.0236486544157484 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2329541_4966897_-1325776 2329541_4966897_-1325776 type=lava mass=10.97417172944396 radius=1.8898232597957598 gravity=307 pressure=244 tempK=1528 oxygen=false locked=true rings=false rotation=9563 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2329541_4966897_-1325776 2329541_4966898_-1325750 type=barren mass=0.049531639951755274 radius=0.4327238813330402 gravity=26 pressure=13 tempK=92 oxygen=false locked=false rings=false rotation=23245 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2329541_4966897_-1325776 2329543_4966897_-1325771 type=barren mass=0.09629020722034799 radius=0.5664330555906398 gravity=30 pressure=1 tempK=208 oxygen=false locked=true rings=false rotation=25004 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2329541_4966897_-1325776 2329543_4966897_-1325774 type=desert mass=0.11412324272634686 radius=0.5911736961048453 gravity=33 pressure=1 tempK=272 oxygen=false locked=true rings=false rotation=23746 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2329541_4966897_-1325776 2329577_4966896_-1325777 type=gasgiant mass=239.81712962466773 radius=9.729968087372685 gravity=253 pressure=1600 tempK=152 oxygen=false locked=false rings=true rotation=12847 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2329541_4966897_-1325776 2329605_4966897_-1325785 type=icegiant mass=265.8158372253331 radius=10.17528264925558 gravity=257 pressure=1600 tempK=113 oxygen=false locked=false rings=true rotation=5832 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2329541_4966897_-1325776 2329644_4966893_-1325768 type=ice mass=0.0654470383830132 radius=0.48744795473595487 gravity=28 pressure=30 tempK=37 oxygen=false locked=false rings=false rotation=13755 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2889434_-3209715_-1090932 2883106_-3209715_-1087241 type=ice mass=4.9461644443979065 radius=1.5223619472849548 gravity=213 pressure=1600 tempK=15 oxygen=false locked=false rings=false rotation=42163 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2889434_-3209715_-1090932 2889367_-3209711_-1090865 type=gasgiant mass=126.16243733242267 radius=7.359168562793266 gravity=233 pressure=1600 tempK=92 oxygen=false locked=false rings=true rotation=14139 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2889434_-3209715_-1090932 2889422_-3209715_-1090925 type=ice mass=0.04363744413674012 radius=0.41128823750065546 gravity=26 pressure=3 tempK=103 oxygen=false locked=false rings=false rotation=32220 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2889434_-3209715_-1090932 2889434_-3209715_-1090925 type=greenhouse mass=26.407567764613358 radius=2.3898701900773904 gravity=400 pressure=1600 tempK=296 oxygen=false locked=true rings=false rotation=57780 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2889434_-3209715_-1090932 2889434_-3209715_-1090930 type=barren mass=0.002681979042407941 radius=0.20001270394003767 gravity=7 pressure=0 tempK=337 oxygen=false locked=true rings=false rotation=49490 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2889434_-3209715_-1090932 2889434_-3209715_-1090932 type=barren mass=0.002694205359879519 radius=0.20039817314943498 gravity=7 pressure=0 tempK=1068 oxygen=false locked=true rings=false rotation=50226 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2889434_-3209715_-1090932 2889455_-3209716_-1090920 type=gasgiant mass=90.9792302923874 radius=6.384029999980021 gravity=223 pressure=1600 tempK=182 oxygen=false locked=false rings=true rotation=7547 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2889434_-3209715_-1090932 2889556_-3209712_-1091022 type=barren mass=0.011631918397034965 radius=0.2959512062908492 gravity=13 pressure=1 tempK=37 oxygen=false locked=false rings=false rotation=54613 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3182346_-1200494_1223840 3182346_-1200494_1223840 type=ice mass=0.2644150271083819 radius=0.7273830659106353 gravity=50 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=7675 metallicity=0.5518987982242867 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3260852_6822578_1746102 3260852_6822578_1746102 type=ice mass=2.4359543310779155 radius=1.2795628404660984 gravity=149 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=43959 metallicity=1.0755213177926986 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3723419_2156869_-1335565 3723396_2156869_-1335603 type=gasgiant mass=141.0806134433791 radius=7.725594531511088 gravity=236 pressure=1600 tempK=127 oxygen=false locked=false rings=true rotation=12816 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3723419_2156869_-1335565 3723397_2156870_-1335553 type=ice mass=23.366603194962387 radius=2.319630111342125 gravity=400 pressure=1600 tempK=162 oxygen=false locked=false rings=false rotation=6657 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3723419_2156869_-1335565 3723418_2156868_-1335551 type=superearth mass=7.072302498375005 radius=1.633148040692367 gravity=265 pressure=1600 tempK=250 oxygen=false locked=false rings=false rotation=20174 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3723419_2156869_-1335565 3723419_2156869_-1335565 type=barren mass=0.0030701447389803204 radius=0.20847545067095652 gravity=7 pressure=0 tempK=1008 oxygen=false locked=true rings=false rotation=46531 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3723419_2156869_-1335565 3723422_2156869_-1335564 type=barren mass=0.002966178789951872 radius=0.20888798108361883 gravity=7 pressure=0 tempK=231 oxygen=false locked=true rings=false rotation=83304 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3723419_2156869_-1335565 3723441_2156872_-1335633 type=superearth mass=13.304249276732389 radius=2.0003350115729988 gravity=332 pressure=1600 tempK=109 oxygen=false locked=false rings=false rotation=26917 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3822552_4765867_-2982992 3822552_4765867_-2982992 type=barren mass=0.2159765047044719 radius=0.7075016622871881 gravity=43 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=17181 metallicity=1.387776541310441 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3927952_4810457_3631655 3924557_4810385_3631323 type=gasgiant mass=133.11218187197326 radius=7.532755154412924 gravity=235 pressure=1600 tempK=106 oxygen=false locked=false rings=true rotation=12677 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3927952_4810457_3631655 3926914_4810506_3631484 type=ice mass=4.26622428294551 radius=1.4188668287815471 gravity=212 pressure=1600 tempK=182 oxygen=false locked=false rings=false rotation=7788 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3927952_4810457_3631655 3927727_4810461_3631449 type=gasgiant mass=47.89753168313653 radius=4.830052167417934 gravity=205 pressure=1600 tempK=357 oxygen=false locked=false rings=true rotation=13722 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3927952_4810457_3631655 3927882_4810463_3631809 type=barren mass=0.00426452682722637 radius=0.2179148571877407 gravity=9 pressure=0 tempK=245 oxygen=false locked=false rings=false rotation=16477 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3927952_4810457_3631655 3927952_4810457_3631655 type=unclassified mass=0.07525681150547835 radius=0.47039909734528434 gravity=34 pressure=0 tempK=6985 oxygen=false locked=true rings=false rotation=37231 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3927952_4810457_3631655 3927997_4810459_3631662 type=lava mass=16.96892069408671 radius=2.078438494405952 gravity=393 pressure=1600 tempK=1078 oxygen=false locked=false rings=false rotation=61020 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3927952_4810457_3631655 3933209_4810411_3630182 type=gasgiant mass=24.08121298114381 radius=3.581876560847876 gravity=188 pressure=1600 tempK=84 oxygen=false locked=false rings=true rotation=6252 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3991853_-3409879_1695966 3991812_-3409882_1696009 type=ice mass=0.0027299710707084555 radius=0.2000049072546915 gravity=7 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=26118 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3991853_-3409879_1695966 3991834_-3409881_1695997 type=ice mass=22.304600037860514 radius=2.428323573235008 gravity=378 pressure=1600 tempK=123 oxygen=false locked=false rings=false rotation=14789 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3991853_-3409879_1695966 3991853_-3409879_1695966 type=lava mass=2.8230307532429397 radius=1.362518247910563 gravity=152 pressure=66 tempK=948 oxygen=false locked=true rings=false rotation=6325 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3991853_-3409879_1695966 3991853_-3409879_1695970 type=exotic mass=0.7368209632598757 radius=0.9316990840993771 gravity=85 pressure=239 tempK=274 oxygen=false locked=true rings=false rotation=82320 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3991853_-3409879_1695966 3991854_-3409879_1695962 type=barren mass=0.0036750378630961253 radius=0.22033987090436358 gravity=8 pressure=0 tempK=208 oxygen=false locked=true rings=false rotation=6613 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3991853_-3409879_1695966 3991854_-3409879_1695966 type=barren mass=0.004100190374783057 radius=0.22662800708790426 gravity=8 pressure=0 tempK=328 oxygen=false locked=true rings=false rotation=6811 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3991853_-3409879_1695966 3991859_-3409879_1695970 type=gasgiant mass=202.86185569294582 radius=9.047143938187649 gravity=248 pressure=1600 tempK=302 oxygen=false locked=false rings=false rotation=6288 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3991853_-3409879_1695966 3991868_-3409878_1695958 type=ice mass=1.9070388107758682 radius=1.2416552219403456 gravity=124 pressure=1600 tempK=178 oxygen=false locked=false rings=false rotation=6048 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3991853_-3409879_1695966 3991928_-3409882_1696026 type=barren mass=0.062140444967901935 radius=0.48401663040802284 gravity=27 pressure=13 tempK=41 oxygen=false locked=false rings=false rotation=63759 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4713087_-1117360_-531483 4713087_-1117360_-531483 type=barren mass=0.1246751408394732 radius=0.5492908503901086 gravity=41 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=42693 metallicity=0.39650211260948054 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5671190_876631_2238047 5671190_876631_2238047 type=ice mass=0.023687324890898958 radius=0.3623353932076434 gravity=18 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=24681 metallicity=0.48458331719113257 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6440405_-2011426_5750662 6440405_-2011426_5750662 type=superearth mass=13.786999315655324 radius=2.1711386375337 gravity=292 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=16921 metallicity=1.3836626361029771 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6464859_1421762_4096630 6464859_1421762_4096630 type=ice mass=3.7177160066414987 radius=1.3688524064028083 gravity=198 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=18789 metallicity=0.6218013164273715 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 662972_1432839_1639281 662972_1432839_1639281 type=ice mass=10.857475966415414 radius=1.8066540452223296 gravity=333 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=61901 metallicity=0.6395580353459209 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6924773_4236388_1837031 6924773_4236388_1837031 type=ice mass=0.005876345400979748 radius=0.25767849632181145 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=62110 metallicity=0.5876410185903691 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 793862_2575944_-859446 793862_2575944_-859446 type=ice mass=0.08539001171147081 radius=0.5129142760947367 gravity=32 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=14623 metallicity=0.7867155683259348 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1354226_-775980_5896155 id=-245847961 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless + system -1786692_-493284_1495624 id=-1420304977 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless + system -2263975_2500210_-512704 id=-544380065 kind=ROGUE_PLANET name=PGR--3525313.0.-3525313 starless + system -2453677_750464_5429323 id=-1103945881 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless + system -2492763_-2673435_-1919668 id=-115633053 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless + system -2783529_5420979_-114688 id=-301293885 kind=ROGUE_PLANET name=PGR--3525313.3525313.-3525313 starless + system -3019098_4194427_955152 id=-203567605 kind=ROGUE_PLANET name=PGR--3525313.3525313.0 starless + system -917361_6648687_4231175 id=-1415803897 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system -987441_2574102_354383 id=-1653738961 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless + system 1192693_4180823_6715391 id=-1464588085 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless + system 137508_498291_5548492 id=-1629913369 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless + system 1926001_-2868405_6450292 id=-1724907213 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless + system 2329541_4966897_-1325776 id=-1372746905 kind=STAR name=PGS-0.3525313.-3525313 starTemp=40 starSize=0.9957200884819031 + system 2889434_-3209715_-1090932 id=-1616632873 kind=STAR name=PGS-0.-3525313.-3525313 starTemp=40 starSize=0.972520649433136 + system 3182346_-1200494_1223840 id=-1628626657 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless + system 3260852_6822578_1746102 id=-24266345 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless + system 3723419_2156869_-1335565 id=-392697649 kind=STAR name=PGS-3525313.0.-3525313 starTemp=40 starSize=0.8668519854545593 + system 3822552_4765867_-2982992 id=-646721517 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless + system 3927952_4810457_3631655 id=-771612361 kind=STAR name=PGS-3525313.3525313.3525313 starTemp=220 starSize=1.54029381275177 + system 3991853_-3409879_1695966 id=-532811557 kind=STAR name=PGS-3525313.-3525313.0 starTemp=40 starSize=0.7370571494102478 + system 4713087_-1117360_-531483 id=-1273646913 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless + system 5671190_876631_2238047 id=-1812954729 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless + system 6440405_-2011426_5750662 id=-929483845 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless + system 6464859_1421762_4096630 id=-1373082229 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless + system 662972_1432839_1639281 id=-576770217 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 6924773_4236388_1837031 id=-1211781741 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless + system 793862_2575944_-859446 id=-508002589 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless seed -1 systems=27 - body -1318696_8986598_-3915037 -1317886_8986631_-3914953 kind=ASTEROID_BELT orbit=4356 radius=0.0 starId=-905285817 frame=true - body -1318696_8986598_-3915037 -1318191_8986622_-3914975 kind=GAS_GIANT orbit=2723 radius=9.166746324818572 starId=-905285817 frame=true - body -1318696_8986598_-3915037 -1318607_8986594_-3915071 kind=GAS_GIANT orbit=512 radius=10.777387057228022 starId=-905285817 frame=true - body -1318696_8986598_-3915037 -1318607_8986594_-3915071 kind=MOON orbit=512 radius=0.23392988329428063 starId=-905285817 frame=false - body -1318696_8986598_-3915037 -1318607_8986594_-3915071 kind=MOON orbit=512 radius=0.32810033747221057 starId=-905285817 frame=false - body -1318696_8986598_-3915037 -1318607_8986594_-3915071 kind=MOON orbit=512 radius=0.5111663133840638 starId=-905285817 frame=false - body -1318696_8986598_-3915037 -1318665_8986600_-3914994 kind=ASTEROID_BELT orbit=284 radius=0.0 starId=-905285817 frame=true - body -1318696_8986598_-3915037 -1318688_8986598_-3915034 kind=PLANET orbit=44 radius=0.9968042979223011 starId=-905285817 frame=true - body -1318696_8986598_-3915037 -1318696_8986598_-3915037 kind=STAR orbit=0 radius=0.0 starId=-905285817 frame=true - body -1318696_8986598_-3915037 -1318724_8986598_-3915032 kind=STAR orbit=152 radius=91.55521749079227 starId=-905285818 frame=true - body -1353458_-2041995_3669964 -1353458_-2041995_3669964 kind=MOON orbit=0 radius=0.5324815188458033 starId=-1793186305 frame=false - body -1353458_-2041995_3669964 -1353458_-2041995_3669964 kind=MOON orbit=0 radius=1.4461590339975599 starId=-1793186305 frame=false - body -1353458_-2041995_3669964 -1353458_-2041995_3669964 kind=ROGUE_PLANET orbit=0 radius=0.9643703406893043 starId=-1793186305 frame=true - body -1504868_3859101_7238142 -1504868_3859101_7238142 kind=ROGUE_PLANET orbit=0 radius=0.2032533764928848 starId=-731140245 frame=true - body -1702786_5224077_9097589 -1702786_5224077_9097589 kind=MOON orbit=0 radius=0.3763027176750806 starId=-1673402021 frame=false - body -1702786_5224077_9097589 -1702786_5224077_9097589 kind=ROGUE_PLANET orbit=0 radius=1.742200393255895 starId=-1673402021 frame=true - body -2157794_-3988883_-1548825 -2157794_-3988883_-1548825 kind=ROGUE_PLANET orbit=0 radius=0.2269790512853808 starId=-1640872829 frame=true - body -362968_4023206_-1712631 -362968_4023206_-1712631 kind=ROGUE_PLANET orbit=0 radius=0.20672927136756797 starId=-429342865 frame=true - body -4110886_-2675466_7716656 -4110886_-2675466_7716656 kind=ROGUE_PLANET orbit=0 radius=0.8552568554482771 starId=-305214521 frame=true - body -4807070_1870953_3722184 -4807070_1870953_3722184 kind=MOON orbit=0 radius=1.1039665790614814 starId=-796796053 frame=false - body -4807070_1870953_3722184 -4807070_1870953_3722184 kind=ROGUE_PLANET orbit=0 radius=1.2754301329722684 starId=-796796053 frame=true - body -914407_6480482_4740872 -914397_6480482_4740869 kind=PLANET orbit=55 radius=1.752767775052126 starId=-1744473825 frame=true - body -914407_6480482_4740872 -914407_6480482_4740872 kind=STAR orbit=0 radius=0.0 starId=-1744473825 frame=true - body -914407_6480482_4740872 -914411_6480482_4740906 kind=STAR orbit=185 radius=103.22470710575581 starId=-1744473826 frame=true - body -914407_6480482_4740872 -914426_6480521_4740081 kind=ASTEROID_BELT orbit=4236 radius=0.0 starId=-1744473825 frame=true - body -914407_6480482_4740872 -914619_6480479_4740425 kind=PLANET orbit=2648 radius=1.3312106788774432 starId=-1744473825 frame=true - body 102729_-4691975_-1824860 102729_-4691975_-1824860 kind=MOON orbit=0 radius=0.4858620267121579 starId=-986449737 frame=false - body 102729_-4691975_-1824860 102729_-4691975_-1824860 kind=ROGUE_PLANET orbit=0 radius=1.2514467561785527 starId=-986449737 frame=true - body 1386765_1777561_3866474 1386765_1777561_3866474 kind=MOON orbit=0 radius=0.7692105872285928 starId=-1806957165 frame=false - body 1386765_1777561_3866474 1386765_1777561_3866474 kind=ROGUE_PLANET orbit=0 radius=0.6469067641925179 starId=-1806957165 frame=true - body 1414282_6629949_2442943 1414282_6629949_2442943 kind=MOON orbit=0 radius=0.6090133131739475 starId=-1333921245 frame=false - body 1414282_6629949_2442943 1414282_6629949_2442943 kind=MOON orbit=0 radius=2.357964579717853 starId=-1333921245 frame=false - body 1414282_6629949_2442943 1414282_6629949_2442943 kind=ROGUE_PLANET orbit=0 radius=0.2000303552532779 starId=-1333921245 frame=true - body 1932863_-2490869_1402243 1932863_-2490869_1402243 kind=ROGUE_PLANET orbit=0 radius=1.4336462116033544 starId=-681598609 frame=true - body 2030702_-209809_5919874 2028640_-209750_5920429 kind=GAS_GIANT orbit=11425 radius=3.8694011905491568 starId=-129286269 frame=true - body 2030702_-209809_5919874 2029022_-209735_5916898 kind=ASTEROID_BELT orbit=18280 radius=0.0 starId=-129286269 frame=true - body 2030702_-209809_5919874 2030246_-209824_5920264 kind=ASTEROID_BELT orbit=3210 radius=0.0 starId=-129286269 frame=true - body 2030702_-209809_5919874 2030430_-209815_5919472 kind=PLANET orbit=2597 radius=1.5652167800475636 starId=-129286269 frame=true - body 2030702_-209809_5919874 2030438_-209803_5918826 kind=GAS_GIANT orbit=5778 radius=9.482777391861841 starId=-129286269 frame=true - body 2030702_-209809_5919874 2030438_-209803_5918826 kind=MOON orbit=5778 radius=0.23078417705442864 starId=-129286269 frame=false - body 2030702_-209809_5919874 2030438_-209803_5918826 kind=MOON orbit=5778 radius=0.5140558521282359 starId=-129286269 frame=false - body 2030702_-209809_5919874 2030578_-209804_5919889 kind=PLANET orbit=666 radius=1.903584469464533 starId=-129286269 frame=true - body 2030702_-209809_5919874 2030603_-209804_5920078 kind=MOON orbit=1212 radius=0.23597330481517403 starId=-129286269 frame=false - body 2030702_-209809_5919874 2030603_-209804_5920078 kind=PLANET orbit=1212 radius=0.40667920422127396 starId=-129286269 frame=true - body 2030702_-209809_5919874 2030688_-209810_5919897 kind=MOON orbit=147 radius=0.5243609414710944 starId=-129286269 frame=false - body 2030702_-209809_5919874 2030688_-209810_5919897 kind=PLANET orbit=147 radius=2.249378149861664 starId=-129286269 frame=true - body 2030702_-209809_5919874 2030695_-209809_5919888 kind=PLANET orbit=85 radius=2.4215629335179583 starId=-129286269 frame=true - body 2030702_-209809_5919874 2030702_-209809_5919874 kind=STAR orbit=0 radius=0.0 starId=-129286269 frame=true - body 2030702_-209809_5919874 2030705_-209809_5919877 kind=STAR orbit=23 radius=73.34426656901836 starId=-129286270 frame=true - body 2030702_-209809_5919874 2030736_-209808_5919916 kind=MOON orbit=290 radius=0.22565649492379916 starId=-129286269 frame=false - body 2030702_-209809_5919874 2030736_-209808_5919916 kind=PLANET orbit=290 radius=1.3611153732714876 starId=-129286269 frame=true - body 2039398_8275570_9269915 2039398_8275570_9269915 kind=MOON orbit=0 radius=0.4610801615111488 starId=-1830124441 frame=false - body 2039398_8275570_9269915 2039398_8275570_9269915 kind=ROGUE_PLANET orbit=0 radius=0.5235138597237201 starId=-1830124441 frame=true - body 2241385_8907861_-1975542 2241385_8907861_-1975542 kind=ROGUE_PLANET orbit=0 radius=1.6405990388349718 starId=-704831901 frame=true - body 370568_3515329_-2658033 370568_3515329_-2658033 kind=ROGUE_PLANET orbit=0 radius=0.8124595416171261 starId=-1989327477 frame=true - body 4076707_1478856_6199021 4076254_1478945_6196302 kind=ASTEROID_BELT orbit=14750 radius=0.0 starId=-108865093 frame=true - body 4076707_1478856_6199021 4076349_1478856_6198826 kind=STAR orbit=2183 radius=116.66968788027764 starId=-108865094 frame=true - body 4076707_1478856_6199021 4076707_1478856_6199021 kind=STAR orbit=0 radius=0.0 starId=-108865093 frame=true - body 4076707_1478856_6199021 4076712_1478856_6199003 kind=PLANET orbit=102 radius=1.339988705282001 starId=-108865093 frame=true - body 4076707_1478856_6199021 4076783_1478853_6199018 kind=PLANET orbit=406 radius=2.462976278612129 starId=-108865093 frame=true - body 4076707_1478856_6199021 4078344_1478888_6198481 kind=MOON orbit=9219 radius=0.6633388777554012 starId=-108865093 frame=false - body 4076707_1478856_6199021 4078344_1478888_6198481 kind=PLANET orbit=9219 radius=0.3608682099209395 starId=-108865093 frame=true - body 5171696_5557639_1152562 5171696_5557639_1152562 kind=ROGUE_PLANET orbit=0 radius=2.1183410616555247 starId=-1586917565 frame=true - body 5699530_6314857_-235309 5699530_6314857_-235309 kind=ROGUE_PLANET orbit=0 radius=1.6736751477831422 starId=-1764830897 frame=true - body 5799956_-3218726_4876987 5799956_-3218726_4876987 kind=ROGUE_PLANET orbit=0 radius=0.44010709889146316 starId=-780232045 frame=true - body 6058862_2614790_2030893 6056728_2614916_2032616 kind=ASTEROID_BELT orbit=14684 radius=0.0 starId=-1811230377 frame=true - body 6058862_2614790_2030893 6058410_2614864_2029239 kind=GAS_GIANT orbit=9178 radius=3.389605866397197 starId=-1811230377 frame=true - body 6058862_2614790_2030893 6058500_2614795_2030763 kind=GAS_GIANT orbit=2059 radius=6.781850976788818 starId=-1811230377 frame=true - body 6058862_2614790_2030893 6058500_2614795_2030763 kind=MOON orbit=2059 radius=0.2028069061546957 starId=-1811230377 frame=false - body 6058862_2614790_2030893 6058500_2614795_2030763 kind=MOON orbit=2059 radius=0.21047105211003583 starId=-1811230377 frame=false - body 6058862_2614790_2030893 6058500_2614795_2030763 kind=MOON orbit=2059 radius=0.282696328177196 starId=-1811230377 frame=false - body 6058862_2614790_2030893 6058661_2614798_2030967 kind=ASTEROID_BELT orbit=1143 radius=0.0 starId=-1811230377 frame=true - body 6058862_2614790_2030893 6058862_2614790_2030893 kind=STAR orbit=0 radius=0.0 starId=-1811230377 frame=true - body 6058862_2614790_2030893 6058886_2614790_2030901 kind=PLANET orbit=136 radius=0.20651265210298247 starId=-1811230377 frame=true - body 6058862_2614790_2030893 6058957_2614792_2030875 kind=PLANET orbit=519 radius=0.3373745430786347 starId=-1811230377 frame=true - body 6643221_-3045831_-3292025 6643221_-3045831_-3292025 kind=ROGUE_PLANET orbit=0 radius=0.2071390155595182 starId=-383094489 frame=true - body 7759431_-1367342_9293025 7759431_-1367342_9293025 kind=MOON orbit=0 radius=0.25419963926621847 starId=-20326109 frame=false - body 7759431_-1367342_9293025 7759431_-1367342_9293025 kind=MOON orbit=0 radius=0.6283919111941731 starId=-20326109 frame=false - body 7759431_-1367342_9293025 7759431_-1367342_9293025 kind=ROGUE_PLANET orbit=0 radius=2.0339633844371137 starId=-20326109 frame=true - body 8695004_5647655_6839165 8695004_5647655_6839165 kind=MOON orbit=0 radius=0.20787758220781027 starId=-1415522037 frame=false - body 8695004_5647655_6839165 8695004_5647655_6839165 kind=MOON orbit=0 radius=0.35897766141027726 starId=-1415522037 frame=false - body 8695004_5647655_6839165 8695004_5647655_6839165 kind=ROGUE_PLANET orbit=0 radius=2.1417234039988013 starId=-1415522037 frame=true - body 9350867_3953928_9075861 9350867_3953928_9075861 kind=MOON orbit=0 radius=0.6264880419207874 starId=-1168331549 frame=false - body 9350867_3953928_9075861 9350867_3953928_9075861 kind=ROGUE_PLANET orbit=0 radius=0.9930186947160022 starId=-1168331549 frame=true - body 9450953_4739501_-3567012 9450953_4739501_-3567012 kind=MOON orbit=0 radius=1.3571081122505047 starId=-1579933465 frame=false - body 9450953_4739501_-3567012 9450953_4739501_-3567012 kind=MOON orbit=0 radius=2.2125930343742883 starId=-1579933465 frame=false - body 9450953_4739501_-3567012 9450953_4739501_-3567012 kind=ROGUE_PLANET orbit=0 radius=1.6547219994680025 starId=-1579933465 frame=true - derived -1318696_8986598_-3915037 -1317886_8986631_-3914953 type=ice mass=1.1403118528659018 radius=1.0610125397988572 gravity=101 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=58028 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1318696_8986598_-3915037 -1318191_8986622_-3914975 type=gasgiant mass=209.0831017618138 radius=9.166746324818572 gravity=249 pressure=1600 tempK=103 oxygen=false locked=false rings=true rotation=4875 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1318696_8986598_-3915037 -1318607_8986594_-3915071 type=gasgiant mass=303.39257559903837 radius=10.777387057228022 gravity=261 pressure=1600 tempK=239 oxygen=false locked=false rings=true rotation=9130 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1318696_8986598_-3915037 -1318665_8986600_-3914994 type=superearth mass=3.2060392517452287 radius=1.383941748505236 gravity=167 pressure=1600 tempK=349 oxygen=false locked=false rings=false rotation=39841 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1318696_8986598_-3915037 -1318688_8986598_-3915034 type=desert mass=0.925387669079765 radius=0.9968042979223011 gravity=93 pressure=30 tempK=394 oxygen=false locked=true rings=false rotation=54615 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1318696_8986598_-3915037 -1318696_8986598_-3915037 type=lava mass=9.532602606904776 radius=1.8239998144090523 gravity=287 pressure=21 tempK=2782 oxygen=false locked=true rings=false rotation=16399 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1318696_8986598_-3915037 -1318724_8986598_-3915032 type=greenhouse mass=13.76027159799629 radius=1.9359204601558124 gravity=367 pressure=1600 tempK=369 oxygen=false locked=false rings=false rotation=11197 metallicity=0.432243631425742 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1353458_-2041995_3669964 -1353458_-2041995_3669964 type=ice mass=0.9705727911568036 radius=0.9643703406893043 gravity=104 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=45778 metallicity=1.1214926014420192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1504868_3859101_7238142 -1504868_3859101_7238142 type=ice mass=0.0023310606017554374 radius=0.2032533764928848 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=15869 metallicity=0.38446088590345784 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1702786_5224077_9097589 -1702786_5224077_9097589 type=superearth mass=8.285512268197653 radius=1.742200393255895 gravity=273 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=75847 metallicity=0.5813733269433281 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2157794_-3988883_-1548825 -2157794_-3988883_-1548825 type=ice mass=0.004825891880283745 radius=0.2269790512853808 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=72350 metallicity=0.6467485105947578 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -362968_4023206_-1712631 -362968_4023206_-1712631 type=barren mass=0.0034255630062420845 radius=0.20672927136756797 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=67718 metallicity=1.5756640897851817 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4110886_-2675466_7716656 -4110886_-2675466_7716656 type=ice mass=0.5433354695754443 radius=0.8552568554482771 gravity=74 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=14178 metallicity=1.5521124002335625 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4807070_1870953_3722184 -4807070_1870953_3722184 type=superearth mass=2.7887644245058842 radius=1.2754301329722684 gravity=171 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=8419 metallicity=1.4022218827767885 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -914407_6480482_4740872 -914397_6480482_4740869 type=superearth mass=9.878571415470441 radius=1.752767775052126 gravity=322 pressure=1600 tempK=613 oxygen=false locked=false rings=false rotation=36941 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -914407_6480482_4740872 -914407_6480482_4740872 type=lava mass=7.2416518225481585 radius=1.6596029611154552 gravity=263 pressure=240 tempK=1485 oxygen=false locked=true rings=false rotation=23655 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -914407_6480482_4740872 -914411_6480482_4740906 type=greenhouse mass=1.9521035208502402 radius=1.1875801386423888 gravity=138 pressure=904 tempK=348 oxygen=false locked=false rings=false rotation=74733 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -914407_6480482_4740872 -914426_6480521_4740081 type=superearth mass=2.8468729880125068 radius=1.2666108742092894 gravity=177 pressure=1600 tempK=129 oxygen=false locked=false rings=false rotation=53180 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -914407_6480482_4740872 -914619_6480479_4740425 type=ice mass=3.098920473393558 radius=1.3312106788774432 gravity=175 pressure=1600 tempK=141 oxygen=false locked=false rings=false rotation=43761 metallicity=0.7933806723050685 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 102729_-4691975_-1824860 102729_-4691975_-1824860 type=ice mass=1.900309120104137 radius=1.2514467561785527 gravity=121 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=6450 metallicity=0.6212021465115256 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1386765_1777561_3866474 1386765_1777561_3866474 type=barren mass=0.18218725160447546 radius=0.6469067641925179 gravity=44 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=10565 metallicity=0.8315665430971471 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1414282_6629949_2442943 1414282_6629949_2442943 type=barren mass=0.0028248942891845536 radius=0.2000303552532779 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=27236 metallicity=1.0977908061045079 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1932863_-2490869_1402243 1932863_-2490869_1402243 type=superearth mass=3.6830331610866343 radius=1.4336462116033544 gravity=179 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=15803 metallicity=1.3987381503775116 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2028640_-209750_5920429 type=icegiant mass=28.761040543123272 radius=3.8694011905491568 gravity=192 pressure=1600 tempK=89 oxygen=false locked=false rings=true rotation=9548 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2029022_-209735_5916898 type=gasgiant mass=254.61180685264645 radius=9.98653989270698 gravity=255 pressure=1600 tempK=70 oxygen=false locked=false rings=true rotation=7184 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030246_-209824_5920264 type=barren mass=0.004913333481680003 radius=0.22675389886512917 gravity=10 pressure=0 tempK=86 oxygen=false locked=false rings=false rotation=15500 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030430_-209815_5919472 type=ice mass=4.6864481443899955 radius=1.5652167800475636 gravity=191 pressure=1600 tempK=177 oxygen=false locked=false rings=false rotation=9268 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030438_-209803_5918826 type=icegiant mass=226.03501567053354 radius=9.482777391861841 gravity=251 pressure=1600 tempK=125 oxygen=false locked=false rings=true rotation=6807 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030578_-209804_5919889 type=greenhouse mass=8.230783879096062 radius=1.903584469464533 gravity=227 pressure=1600 tempK=310 oxygen=false locked=false rings=false rotation=46326 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030603_-209804_5920078 type=barren mass=0.0392839495614152 radius=0.40667920422127396 gravity=24 pressure=3 tempK=140 oxygen=false locked=false rings=false rotation=11624 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030688_-209810_5919897 type=superearth mass=17.775507536447584 radius=2.249378149861664 gravity=351 pressure=1600 tempK=851 oxygen=false locked=false rings=false rotation=24739 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030695_-209809_5919888 type=greenhouse mass=25.397641051281813 radius=2.4215629335179583 gravity=400 pressure=1600 tempK=855 oxygen=false locked=false rings=false rotation=44158 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030702_-209809_5919874 type=lava mass=4.776528115855772 radius=1.5061865406505028 gravity=211 pressure=44 tempK=1147 oxygen=false locked=true rings=false rotation=8495 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030705_-209809_5919877 type=lava mass=0.005404841629779387 radius=0.2632067849202486 gravity=8 pressure=0 tempK=861 oxygen=false locked=true rings=false rotation=23288 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2030702_-209809_5919874 2030736_-209808_5919916 type=greenhouse mass=3.2082270933212507 radius=1.3611153732714876 gravity=173 pressure=1059 tempK=424 oxygen=false locked=false rings=true rotation=10818 metallicity=0.6281587930968467 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2039398_8275570_9269915 2039398_8275570_9269915 type=ice mass=0.06958788439052067 radius=0.5235138597237201 gravity=25 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=9548 metallicity=1.4356709862180739 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2241385_8907861_-1975542 2241385_8907861_-1975542 type=ice mass=5.733141562078599 radius=1.6405990388349718 gravity=213 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=18439 metallicity=0.9981792538177909 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 370568_3515329_-2658033 370568_3515329_-2658033 type=ice mass=0.5620845235010506 radius=0.8124595416171261 gravity=85 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=13437 metallicity=0.45217268079600526 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4076707_1478856_6199021 4076254_1478945_6196302 type=barren mass=0.003007142527453317 radius=0.2192772368225473 gravity=6 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=8692 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4076707_1478856_6199021 4076349_1478856_6198826 type=icegiant mass=291.8884858342837 radius=10.597766739631862 gravity=260 pressure=1600 tempK=202 oxygen=false locked=false rings=true rotation=7338 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4076707_1478856_6199021 4076707_1478856_6199021 type=lava mass=1.3487688784875655 radius=1.021677182631749 gravity=129 pressure=0 tempK=4794 oxygen=false locked=true rings=false rotation=28070 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4076707_1478856_6199021 4076712_1478856_6199003 type=desert mass=2.660035614293332 radius=1.339988705282001 gravity=148 pressure=137 tempK=542 oxygen=false locked=false rings=false rotation=86895 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4076707_1478856_6199021 4076783_1478853_6199018 type=superearth mass=33.37291552120821 radius=2.462976278612129 gravity=400 pressure=1600 tempK=503 oxygen=false locked=false rings=false rotation=23809 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4076707_1478856_6199021 4078344_1478888_6198481 type=barren mass=0.01924329724609664 radius=0.3608682099209395 gravity=15 pressure=3 tempK=51 oxygen=false locked=false rings=false rotation=31393 metallicity=1.5214842597507379 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5171696_5557639_1152562 5171696_5557639_1152562 type=ice mass=19.82417253834241 radius=2.1183410616555247 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=19426 metallicity=0.6074607533120471 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5699530_6314857_-235309 5699530_6314857_-235309 type=superearth mass=5.126645332062038 radius=1.6736751477831422 gravity=183 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=18291 metallicity=0.7051798242602939 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5799956_-3218726_4876987 5799956_-3218726_4876987 type=ice mass=0.04806093411441581 radius=0.44010709889146316 gravity=25 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=19751 metallicity=1.1250319768630241 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6058862_2614790_2030893 6056728_2614916_2032616 type=ice mass=0.15272835548480254 radius=0.5796954533244889 gravity=45 pressure=190 tempK=45 oxygen=false locked=false rings=true rotation=19823 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6058862_2614790_2030893 6058410_2614864_2029239 type=icegiant mass=21.211291392018328 radius=3.389605866397197 gravity=185 pressure=1600 tempK=102 oxygen=false locked=false rings=true rotation=5442 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6058862_2614790_2030893 6058500_2614795_2030763 type=gasgiant mass=104.55020813243003 radius=6.781850976788818 gravity=227 pressure=1600 tempK=216 oxygen=false locked=false rings=true rotation=5441 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6058862_2614790_2030893 6058661_2614798_2030967 type=gasgiant mass=311.7431754492103 radius=10.905370864836637 gravity=262 pressure=1600 tempK=291 oxygen=false locked=false rings=false rotation=6196 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6058862_2614790_2030893 6058862_2614790_2030893 type=lava mass=29.975680424216204 radius=2.438270655680055 gravity=400 pressure=98 tempK=5672 oxygen=false locked=true rings=false rotation=50008 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6058862_2614790_2030893 6058886_2614790_2030901 type=barren mass=0.002408548470581327 radius=0.20651265210298247 gravity=6 pressure=0 tempK=432 oxygen=false locked=false rings=false rotation=16630 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6058862_2614790_2030893 6058957_2614792_2030875 type=ice mass=0.01574027225297693 radius=0.3373745430786347 gravity=14 pressure=0 tempK=181 oxygen=false locked=false rings=false rotation=20393 metallicity=1.5057991364173167 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6643221_-3045831_-3292025 6643221_-3045831_-3292025 type=barren mass=0.002889217083861708 radius=0.2071390155595182 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=60295 metallicity=0.43353637696850833 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7759431_-1367342_9293025 7759431_-1367342_9293025 type=ice mass=12.12677071434844 radius=2.0339633844371137 gravity=293 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=17385 metallicity=1.2409342000592618 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8695004_5647655_6839165 8695004_5647655_6839165 type=superearth mass=16.511656067843727 radius=2.1417234039988013 gravity=360 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=24555 metallicity=0.5487508545154615 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9350867_3953928_9075861 9350867_3953928_9075861 type=ice mass=0.9698264187238621 radius=0.9930186947160022 gravity=98 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=18465 metallicity=0.5426956430223249 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9450953_4739501_-3567012 9450953_4739501_-3567012 type=ice mass=7.087855370751103 radius=1.6547219994680025 gravity=259 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=6260 metallicity=0.4540346624894932 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1318696_8986598_-3915037 id=-905285817 kind=STAR name=PGS--5002361.5002361.-5002361 starTemp=100 starSize=1.042992115020752 - system -1353458_-2041995_3669964 id=-1793186305 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless - system -1504868_3859101_7238142 id=-731140245 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless - system -1702786_5224077_9097589 id=-1673402021 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless - system -2157794_-3988883_-1548825 id=-1640872829 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless - system -362968_4023206_-1712631 id=-429342865 kind=ROGUE_PLANET name=PGR--5002361.0.-5002361 starless - system -4110886_-2675466_7716656 id=-305214521 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless - system -4807070_1870953_3722184 id=-796796053 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless - system -914407_6480482_4740872 id=-1744473825 kind=STAR name=PGS--5002361.5002361.0 starTemp=40 starSize=0.9455409646034241 - system 102729_-4691975_-1824860 id=-986449737 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless - system 1386765_1777561_3866474 id=-1806957165 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 1414282_6629949_2442943 id=-1333921245 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless - system 1932863_-2490869_1402243 id=-681598609 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless - system 2030702_-209809_5919874 id=-129286269 kind=STAR name=PGS-0.-5002361.5002361 starTemp=40 starSize=0.6718353629112244 - system 2039398_8275570_9269915 id=-1830124441 kind=ROGUE_PLANET name=PGR-0.5002361.5002361 starless - system 2241385_8907861_-1975542 id=-704831901 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless - system 370568_3515329_-2658033 id=-1989327477 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless - system 4076707_1478856_6199021 id=-108865093 kind=STAR name=PGS-0.0.5002361 starTemp=150 starSize=1.3768268823623657 - system 5171696_5557639_1152562 id=-1586917565 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless - system 5699530_6314857_-235309 id=-1764830897 kind=ROGUE_PLANET name=PGR-5002361.5002361.-5002361 starless - system 5799956_-3218726_4876987 id=-780232045 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless - system 6058862_2614790_2030893 id=-1811230377 kind=STAR name=PGS-5002361.0.0 starTemp=150 starSize=1.5379765033721924 - system 6643221_-3045831_-3292025 id=-383094489 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless - system 7759431_-1367342_9293025 id=-20326109 kind=ROGUE_PLANET name=PGR-5002361.-5002361.5002361 starless - system 8695004_5647655_6839165 id=-1415522037 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless - system 9350867_3953928_9075861 id=-1168331549 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless - system 9450953_4739501_-3567012 id=-1579933465 kind=ROGUE_PLANET name=PGR-5002361.0.-5002361 starless + body -1080086_1555356_4409113 -1080086_1555356_4409113 kind=MOON orbit=0 radius=0.4512046985653989 starId=-1337008461 frame=false at=470919,0,-377582 + body -1080086_1555356_4409113 -1080086_1555356_4409113 kind=MOON orbit=0 radius=8.82444722408132 starId=-1337008461 frame=false at=-336265,0,-129669 + body -1080086_1555356_4409113 -1080086_1555356_4409113 kind=ROGUE_PLANET orbit=0 radius=2.038268124956373 starId=-1337008461 frame=true at=0,0,0 + body -1834664_1670352_-1563056 -1834341_1670330_-1563572 kind=PLANET orbit=3256 radius=1.1181559977969782 starId=-1729416869 frame=true at=0,0,0 + body -1834664_1670352_-1563056 -1834603_1670354_-1563099 kind=GAS_GIANT orbit=399 radius=5.676685494197384 starId=-1729416869 frame=true at=0,0,0 + body -1834664_1670352_-1563056 -1834603_1670354_-1563099 kind=MOON orbit=399 radius=0.35039969689273204 starId=-1729416869 frame=false at=1594260,0,688054 + body -1834664_1670352_-1563056 -1834656_1670351_-1563015 kind=ASTEROID_BELT orbit=221 radius=0.0 starId=-1729416869 frame=true at=0,0,0 + body -1834664_1670352_-1563056 -1834664_1670352_-1563056 kind=STAR orbit=0 radius=0.0 starId=-1729416869 frame=true at=0,0,0 + body -1834664_1670352_-1563056 -1834665_1670352_-1563062 kind=MOON orbit=34 radius=0.29895177745722334 starId=-1729416869 frame=false at=-50977,0,-32767 + body -1834664_1670352_-1563056 -1834665_1670352_-1563062 kind=PLANET orbit=34 radius=0.217838595089208 starId=-1729416869 frame=true at=0,0,0 + body -1834664_1670352_-1563056 -1834674_1670352_-1563044 kind=PLANET orbit=84 radius=0.2706016219811198 starId=-1729416869 frame=true at=0,0,0 + body -1834664_1670352_-1563056 -1834697_1670352_-1563082 kind=MOON orbit=225 radius=0.5956547429113542 starId=-1729416869 frame=false at=120313,0,33908 + body -1834664_1670352_-1563056 -1834697_1670352_-1563082 kind=MOON orbit=225 radius=0.7447388998696458 starId=-1729416869 frame=false at=-228355,0,-212304 + body -1834664_1670352_-1563056 -1834697_1670352_-1563082 kind=PLANET orbit=225 radius=1.5301172792246798 starId=-1729416869 frame=true at=0,0,0 + body -1834664_1670352_-1563056 -1834844_1670355_-1562989 kind=MOON orbit=1027 radius=0.292506707547793 starId=-1729416869 frame=false at=-48718,0,38024 + body -1834664_1670352_-1563056 -1834844_1670355_-1562989 kind=MOON orbit=1027 radius=0.3566389214674198 starId=-1729416869 frame=false at=63702,0,-41449 + body -1834664_1670352_-1563056 -1834844_1670355_-1562989 kind=PLANET orbit=1027 radius=0.4169178260700219 starId=-1729416869 frame=true at=0,0,0 + body -1834664_1670352_-1563056 -1835637_1670333_-1563103 kind=ASTEROID_BELT orbit=5209 radius=0.0 starId=-1729416869 frame=true at=0,0,0 + body -2208092_-1790276_1685270 -2208092_-1790276_1685270 kind=ROGUE_PLANET orbit=0 radius=0.2385909118845583 starId=-1765959065 frame=true at=0,0,0 + body -2290821_1403075_856935 -2290821_1403075_856935 kind=MOON orbit=0 radius=1.3548125810973575 starId=-1992245269 frame=false at=-78270,0,-90432 + body -2290821_1403075_856935 -2290821_1403075_856935 kind=ROGUE_PLANET orbit=0 radius=0.6825852386298956 starId=-1992245269 frame=true at=0,0,0 + body -3014847_-1420592_5288623 -3014847_-1420592_5288623 kind=ROGUE_PLANET orbit=0 radius=0.2259652104199226 starId=-794196001 frame=true at=0,0,0 + body -3374315_4976731_3972240 -3374315_4976731_3972240 kind=MOON orbit=0 radius=0.4649359539187697 starId=-1160817649 frame=false at=-20116,0,81144 + body -3374315_4976731_3972240 -3374315_4976731_3972240 kind=ROGUE_PLANET orbit=0 radius=1.257168129941409 starId=-1160817649 frame=true at=0,0,0 + body -436060_-2366191_-949510 -435942_-2366191_-949421 kind=STAR orbit=791 radius=91.17783525288105 starId=-498839402 frame=true at=0,0,0 + body -436060_-2366191_-949510 -436000_-2366193_-949507 kind=ASTEROID_BELT orbit=320 radius=0.0 starId=-498839401 frame=true at=0,0,0 + body -436060_-2366191_-949510 -436025_-2366191_-949523 kind=PLANET orbit=200 radius=1.9765339194370526 starId=-498839401 frame=true at=0,0,0 + body -436060_-2366191_-949510 -436055_-2366191_-949514 kind=PLANET orbit=34 radius=0.34512975441876653 starId=-498839401 frame=true at=0,0,0 + body -436060_-2366191_-949510 -436060_-2366191_-949510 kind=STAR orbit=0 radius=0.0 starId=-498839401 frame=true at=0,0,0 + body -436060_-2366191_-949510 -436062_-2366191_-949508 kind=PLANET orbit=13 radius=0.98242013651816 starId=-498839401 frame=true at=0,0,0 + body -436060_-2366191_-949510 -436065_-2366191_-949500 kind=PLANET orbit=62 radius=1.334015205340586 starId=-498839401 frame=true at=0,0,0 + body -528291_6799917_1589264 -528281_6799917_1589255 kind=MOON orbit=70 radius=0.30983974710458184 starId=-1828554265 frame=false at=-210799,0,96410 + body -528291_6799917_1589264 -528281_6799917_1589255 kind=PLANET orbit=70 radius=1.549060693570877 starId=-1828554265 frame=true at=0,0,0 + body -528291_6799917_1589264 -528289_6799918_1589188 kind=MOON orbit=406 radius=0.2770804126829593 starId=-1828554265 frame=false at=151276,0,-77120 + body -528291_6799917_1589264 -528289_6799918_1589188 kind=MOON orbit=406 radius=0.5931675694487508 starId=-1828554265 frame=false at=-84044,0,-7748 + body -528291_6799917_1589264 -528289_6799918_1589188 kind=PLANET orbit=406 radius=1.2598310614277537 starId=-1828554265 frame=true at=0,0,0 + body -528291_6799917_1589264 -528291_6799917_1589264 kind=STAR orbit=0 radius=0.0 starId=-1828554265 frame=true at=0,0,0 + body -528291_6799917_1589264 -528415_6799932_1589887 kind=MOON orbit=3397 radius=0.21455561212610527 starId=-1828554265 frame=false at=199862,0,57613 + body -528291_6799917_1589264 -528415_6799932_1589887 kind=MOON orbit=3397 radius=0.2404223211669318 starId=-1828554265 frame=false at=445956,0,-71171 + body -528291_6799917_1589264 -528415_6799932_1589887 kind=PLANET orbit=3397 radius=1.9904254713501455 starId=-1828554265 frame=true at=0,0,0 + body -528291_6799917_1589264 -529048_6799943_1588586 kind=ASTEROID_BELT orbit=5435 radius=0.0 starId=-1828554265 frame=true at=0,0,0 + body -935767_4549081_-1731362 -935767_4549081_-1731362 kind=MOON orbit=0 radius=0.22563842767321993 starId=-1358211061 frame=false at=-7718,0,-51222 + body -935767_4549081_-1731362 -935767_4549081_-1731362 kind=ROGUE_PLANET orbit=0 radius=0.21212769702860157 starId=-1358211061 frame=true at=0,0,0 + body 1683940_-183048_-1702546 1683940_-183048_-1702546 kind=ROGUE_PLANET orbit=0 radius=0.28622983833858173 starId=-167956389 frame=true at=0,0,0 + body 1920315_1641073_-2834001 1920315_1641073_-2834001 kind=ROGUE_PLANET orbit=0 radius=0.5693690184380397 starId=-1131707005 frame=true at=0,0,0 + body 233560_2922383_5010954 233560_2922383_5010954 kind=MOON orbit=0 radius=0.3002036042196137 starId=-1390378301 frame=false at=-396679,0,-164931 + body 233560_2922383_5010954 233560_2922383_5010954 kind=ROGUE_PLANET orbit=0 radius=1.4350700127224734 starId=-1390378301 frame=true at=0,0,0 + body 236691_5203788_1778447 236688_5203788_1778436 kind=STAR orbit=59 radius=92.83249720394612 starId=-1521889894 frame=true at=0,0,0 + body 236691_5203788_1778447 236690_5203788_1778446 kind=MOON orbit=8 radius=0.261912143918765 starId=-1521889893 frame=false at=-49373,0,-157230 + body 236691_5203788_1778447 236690_5203788_1778446 kind=PLANET orbit=8 radius=1.5952759531425913 starId=-1521889893 frame=true at=0,0,0 + body 236691_5203788_1778447 236691_5203788_1778447 kind=STAR orbit=0 radius=0.0 starId=-1521889893 frame=true at=0,0,0 + body 236691_5203788_1778447 236693_5203788_1778447 kind=ASTEROID_BELT orbit=12 radius=0.0 starId=-1521889893 frame=true at=0,0,0 + body 236691_5203788_1778447 236832_5203788_1778492 kind=STAR orbit=789 radius=68.7155103546381 starId=-1521889895 frame=true at=0,0,0 + body 2374829_-1121220_6754955 2369207_-1121220_6758455 kind=STAR orbit=35417 radius=75.04819331288338 starId=-1897301650 frame=true at=0,0,0 + body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=GAS_GIANT orbit=319 radius=3.553184930957361 starId=-1897301649 frame=true at=0,0,0 + body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=MOON orbit=319 radius=0.5595779650053618 starId=-1897301649 frame=false at=369129,0,397964 + body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=MOON orbit=319 radius=0.674835009037887 starId=-1897301649 frame=false at=724610,0,570432 + body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=MOON orbit=319 radius=0.6917501847168781 starId=-1897301649 frame=false at=-511342,0,562524 + body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=MOON orbit=319 radius=0.7215686369589247 starId=-1897301649 frame=false at=978801,0,19754 + body 2374829_-1121220_6754955 2374790_-1121224_6755042 kind=ASTEROID_BELT orbit=510 radius=0.0 starId=-1897301649 frame=true at=0,0,0 + body 2374829_-1121220_6754955 2374827_-1121220_6754949 kind=ASTEROID_BELT orbit=33 radius=0.0 starId=-1897301649 frame=true at=0,0,0 + body 2374829_-1121220_6754955 2374829_-1121220_6754955 kind=STAR orbit=0 radius=0.0 starId=-1897301649 frame=true at=0,0,0 + body 2374829_-1121220_6754955 2374830_-1121220_6754957 kind=PLANET orbit=11 radius=0.2038047543227626 starId=-1897301649 frame=true at=0,0,0 + body 2374829_-1121220_6754955 2374835_-1121220_6754946 kind=GAS_GIANT orbit=61 radius=10.39003058667359 starId=-1897301649 frame=true at=0,0,0 + body 2374829_-1121220_6754955 2374835_-1121220_6754946 kind=MOON orbit=61 radius=0.24251796210681933 starId=-1897301649 frame=false at=263909,0,-694979 + body 2374829_-1121220_6754955 2374835_-1121220_6754946 kind=MOON orbit=61 radius=0.2844468544441377 starId=-1897301649 frame=false at=1902683,0,-1123445 + body 2374829_-1121220_6754955 2374835_-1121220_6754946 kind=MOON orbit=61 radius=0.5393710813599364 starId=-1897301649 frame=false at=-1299806,0,-706072 + body 3353092_-2509636_325419 3353092_-2509636_325419 kind=ROGUE_PLANET orbit=0 radius=0.5025877735087989 starId=-1078102009 frame=true at=0,0,0 + body 3392849_5972712_-904179 3392849_5972712_-904179 kind=MOON orbit=0 radius=0.34153682693735343 starId=-1470468277 frame=false at=323,0,36399 + body 3392849_5972712_-904179 3392849_5972712_-904179 kind=ROGUE_PLANET orbit=0 radius=0.27812854586282826 starId=-1470468277 frame=true at=0,0,0 + body 4690385_6611748_3834194 4690385_6611748_3834194 kind=MOON orbit=0 radius=1.8041026325046543 starId=-284738901 frame=false at=-4009,0,-37185 + body 4690385_6611748_3834194 4690385_6611748_3834194 kind=ROGUE_PLANET orbit=0 radius=0.20246189913346935 starId=-284738901 frame=true at=0,0,0 + body 4725059_6081849_-1115135 4725059_6081849_-1115135 kind=MOON orbit=0 radius=2.3125564194620427 starId=-28341585 frame=false at=44007,0,-10316 + body 4725059_6081849_-1115135 4725059_6081849_-1115135 kind=ROGUE_PLANET orbit=0 radius=0.4079580640952251 starId=-28341585 frame=true at=0,0,0 + body 5148034_-2724950_2998726 5147687_-2724953_2998875 kind=GAS_GIANT orbit=2019 radius=7.996125437815539 starId=-258191809 frame=true at=0,0,0 + body 5148034_-2724950_2998726 5147687_-2724953_2998875 kind=MOON orbit=2019 radius=0.35817784110343465 starId=-258191809 frame=false at=-1036390,0,1861088 + body 5148034_-2724950_2998726 5147687_-2724953_2998875 kind=MOON orbit=2019 radius=0.7157249758231874 starId=-258191809 frame=false at=-134387,0,-574491 + body 5148034_-2724950_2998726 5147811_-2724969_2998165 kind=ASTEROID_BELT orbit=3230 radius=0.0 starId=-258191809 frame=true at=0,0,0 + body 5148034_-2724950_2998726 5147837_-2724951_2998798 kind=ASTEROID_BELT orbit=1121 radius=0.0 starId=-258191809 frame=true at=0,0,0 + body 5148034_-2724950_2998726 5148028_-2724950_2998708 kind=PLANET orbit=102 radius=0.22373207447041826 starId=-258191809 frame=true at=0,0,0 + body 5148034_-2724950_2998726 5148030_-2724949_2998788 kind=PLANET orbit=334 radius=0.7535567326131476 starId=-258191809 frame=true at=0,0,0 + body 5148034_-2724950_2998726 5148034_-2724950_2998726 kind=STAR orbit=0 radius=0.0 starId=-258191809 frame=true at=0,0,0 + body 5549745_1612567_-2459150 5549745_1612567_-2459150 kind=ROGUE_PLANET orbit=0 radius=1.0153534844490044 starId=-910886457 frame=true at=0,0,0 + body 5569465_-2990317_6697828 5569089_-2990307_6697897 kind=PLANET orbit=2047 radius=2.113428500370335 starId=-1028453113 frame=true at=0,0,0 + body 5569465_-2990317_6697828 5569364_-2990322_6697902 kind=GAS_GIANT orbit=671 radius=4.609784144078134 starId=-1028453113 frame=true at=0,0,0 + body 5569465_-2990317_6697828 5569364_-2990322_6697902 kind=MOON orbit=671 radius=0.4880679780728293 starId=-1028453113 frame=false at=-775463,0,91138 + body 5569465_-2990317_6697828 5569364_-2990322_6697902 kind=MOON orbit=671 radius=0.5456917923931366 starId=-1028453113 frame=false at=-628683,0,-330366 + body 5569465_-2990317_6697828 5569437_-2990317_6697833 kind=GAS_GIANT orbit=151 radius=9.634001360033889 starId=-1028453113 frame=true at=0,0,0 + body 5569465_-2990317_6697828 5569462_-2990317_6697826 kind=STAR orbit=19 radius=78.81359558343887 starId=-1028453114 frame=true at=0,0,0 + body 5569465_-2990317_6697828 5569465_-2990317_6697828 kind=STAR orbit=0 radius=0.0 starId=-1028453113 frame=true at=0,0,0 + body 5569465_-2990317_6697828 5569476_-2990318_6697814 kind=PLANET orbit=95 radius=2.1269076094125596 starId=-1028453113 frame=true at=0,0,0 + body 5569465_-2990317_6697828 5569481_-2990317_6697828 kind=ASTEROID_BELT orbit=83 radius=0.0 starId=-1028453113 frame=true at=0,0,0 + body 5569465_-2990317_6697828 5569514_-2990315_6697863 kind=PLANET orbit=324 radius=0.35129067425550137 starId=-1028453113 frame=true at=0,0,0 + body 5569465_-2990317_6697828 5570051_-2990300_6698005 kind=ASTEROID_BELT orbit=3275 radius=0.0 starId=-1028453113 frame=true at=0,0,0 + body 6255448_968120_6199733 6255448_968120_6199733 kind=MOON orbit=0 radius=0.4200773190749709 starId=-886206973 frame=false at=25413,0,7149 + body 6255448_968120_6199733 6255448_968120_6199733 kind=ROGUE_PLANET orbit=0 radius=0.29383472367066443 starId=-886206973 frame=true at=0,0,0 + body 6513943_-990357_-607133 6513943_-990357_-607133 kind=MOON orbit=0 radius=1.0834140861728314 starId=-207608917 frame=false at=41645,0,21788 + body 6513943_-990357_-607133 6513943_-990357_-607133 kind=ROGUE_PLANET orbit=0 radius=0.5014717372599318 starId=-207608917 frame=true at=0,0,0 + body 6544140_1191032_686782 6544001_1191037_686867 kind=ASTEROID_BELT orbit=870 radius=0.0 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544042_1191031_686755 kind=MOON orbit=544 radius=0.31646082195635844 starId=-1188842121 frame=false at=-63556,0,-27372 + body 6544140_1191032_686782 6544042_1191031_686755 kind=PLANET orbit=544 radius=0.3603369419189195 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544063_1191032_686756 kind=GAS_GIANT orbit=436 radius=10.24404348301498 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544063_1191032_686756 kind=MOON orbit=436 radius=0.25995484068675223 starId=-1188842121 frame=false at=1020070,0,961217 + body 6544140_1191032_686782 6544063_1191032_686756 kind=MOON orbit=436 radius=0.6310614677749826 starId=-1188842121 frame=false at=775018,0,-2287263 + body 6544140_1191032_686782 6544127_1191032_686779 kind=MOON orbit=72 radius=0.2873039609709143 starId=-1188842121 frame=false at=-21656,0,5725 + body 6544140_1191032_686782 6544127_1191032_686779 kind=PLANET orbit=72 radius=0.21819393982079735 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544128_1191032_686758 kind=PLANET orbit=142 radius=0.34477288327841055 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544134_1191033_686798 kind=MOON orbit=92 radius=0.46934188484059625 starId=-1188842121 frame=false at=49734,0,5147 + body 6544140_1191032_686782 6544134_1191033_686798 kind=PLANET orbit=92 radius=0.2431769234018869 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544135_1191032_686776 kind=GAS_GIANT orbit=43 radius=5.489376420198329 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544135_1191032_686776 kind=MOON orbit=43 radius=0.6855114250089008 starId=-1188842121 frame=false at=-220475,0,795409 + body 6544140_1191032_686782 6544137_1191032_686776 kind=GAS_GIANT orbit=34 radius=5.445296272973473 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544139_1191031_686761 kind=PLANET orbit=110 radius=0.22450064981738688 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544139_1191032_686779 kind=MOON orbit=16 radius=0.7372001331992204 starId=-1188842121 frame=false at=362801,0,-348114 + body 6544140_1191032_686782 6544139_1191032_686779 kind=PLANET orbit=16 radius=1.8148705728399028 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544139_1191032_686783 kind=MOON orbit=8 radius=0.7273856792771569 starId=-1188842121 frame=false at=381033,0,-319720 + body 6544140_1191032_686782 6544139_1191032_686783 kind=PLANET orbit=8 radius=1.7790736666585254 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544140_1191032_686782 kind=STAR orbit=0 radius=0.0 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544141_1191032_686779 kind=ASTEROID_BELT orbit=18 radius=0.0 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544141_1191032_686782 kind=MOON orbit=7 radius=0.5669808783297221 starId=-1188842121 frame=false at=-124752,0,-517372 + body 6544140_1191032_686782 6544141_1191032_686782 kind=PLANET orbit=7 radius=2.0090225676405997 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544141_1191032_686784 kind=PLANET orbit=14 radius=1.7709663722662257 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544142_1191032_686781 kind=MOON orbit=11 radius=0.6023857494894139 starId=-1188842121 frame=false at=-328060,0,191772 + body 6544140_1191032_686782 6544142_1191032_686781 kind=PLANET orbit=11 radius=1.9402070673972158 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544142_1191032_686785 kind=PLANET orbit=21 radius=1.4212622127384715 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544145_1191032_686780 kind=PLANET orbit=28 radius=0.8401221729861281 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544146_1191032_686775 kind=PLANET orbit=52 radius=2.1414268787063544 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544158_1191032_686742 kind=PLANET orbit=232 radius=1.0270189716791964 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544172_1191032_686773 kind=MOON orbit=176 radius=0.7394920664400455 starId=-1188842121 frame=false at=148666,0,-34426 + body 6544140_1191032_686782 6544172_1191032_686773 kind=PLANET orbit=176 radius=0.8104176080813175 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544189_1191032_686805 kind=GAS_GIANT orbit=288 radius=10.446483309221662 starId=-1188842121 frame=true at=0,0,0 + body 6544140_1191032_686782 6544189_1191032_686805 kind=MOON orbit=288 radius=0.22806100230671833 starId=-1188842121 frame=false at=3049369,0,-37540 + body 6544140_1191032_686782 6544189_1191032_686805 kind=MOON orbit=288 radius=0.3903869875297332 starId=-1188842121 frame=false at=-2469126,0,-1818977 + body 6544140_1191032_686782 6544204_1191032_686802 kind=PLANET orbit=357 radius=0.20723184818615456 starId=-1188842121 frame=true at=0,0,0 + body 6799915_4871887_422782 6799897_4871887_422780 kind=GAS_GIANT orbit=95 radius=9.677622955960413 starId=-15767913 frame=true at=0,0,0 + body 6799915_4871887_422782 6799897_4871887_422780 kind=MOON orbit=95 radius=0.42484617416659975 starId=-15767913 frame=false at=1815244,0,-1531196 + body 6799915_4871887_422782 6799906_4871885_422865 kind=ASTEROID_BELT orbit=446 radius=0.0 starId=-15767913 frame=true at=0,0,0 + body 6799915_4871887_422782 6799913_4871887_422790 kind=GAS_GIANT orbit=43 radius=9.568051622775512 starId=-15767913 frame=true at=0,0,0 + body 6799915_4871887_422782 6799914_4871887_422781 kind=PLANET orbit=7 radius=1.103751530221875 starId=-15767913 frame=true at=0,0,0 + body 6799915_4871887_422782 6799915_4871887_422782 kind=STAR orbit=0 radius=0.0 starId=-15767913 frame=true at=0,0,0 + body 6799915_4871887_422782 6799916_4871887_422785 kind=MOON orbit=20 radius=0.25984113495172845 starId=-15767913 frame=false at=301214,0,-263496 + body 6799915_4871887_422782 6799916_4871887_422785 kind=PLANET orbit=20 radius=2.4101333906778977 starId=-15767913 frame=true at=0,0,0 + body 6799915_4871887_422782 6799919_4871887_422780 kind=ASTEROID_BELT orbit=23 radius=0.0 starId=-15767913 frame=true at=0,0,0 + body 6799915_4871887_422782 6799949_4871888_422822 kind=PLANET orbit=279 radius=0.2023591304452867 starId=-15767913 frame=true at=0,0,0 + body 772836_5636740_4927735 772836_5636740_4927735 kind=ROGUE_PLANET orbit=0 radius=0.8207995700352966 starId=-1020620017 frame=true at=0,0,0 + body 907102_1605031_1799741 907102_1605031_1799741 kind=MOON orbit=0 radius=1.6079605624709656 starId=-1806957165 frame=false at=-96755,0,540000 + body 907102_1605031_1799741 907102_1605031_1799741 kind=ROGUE_PLANET orbit=0 radius=2.2676109846054535 starId=-1806957165 frame=true at=0,0,0 + derived -1080086_1555356_4409113 -1080086_1555356_4409113 type=ice mass=16.50279570286089 radius=2.038268124956373 gravity=397 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=8240 metallicity=0.9541592391020977 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1834664_1670352_-1563056 -1834341_1670330_-1563572 type=ice mass=1.3589831485054766 radius=1.1181559977969782 gravity=109 pressure=1600 tempK=90 oxygen=false locked=false rings=false rotation=8770 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1834664_1670352_-1563056 -1834603_1670354_-1563099 type=gasgiant mass=69.44517257650985 radius=5.676685494197384 gravity=216 pressure=1600 tempK=271 oxygen=false locked=false rings=true rotation=5422 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1834664_1670352_-1563056 -1834656_1670351_-1563015 type=superearth mass=5.604436130311837 radius=1.6484907919486196 gravity=206 pressure=1600 tempK=397 oxygen=false locked=false rings=false rotation=55647 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1834664_1670352_-1563056 -1834664_1670352_-1563056 type=lava mass=6.297278147697221 radius=1.5654725816445216 gravity=257 pressure=19 tempK=2796 oxygen=false locked=true rings=false rotation=45683 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1834664_1670352_-1563056 -1834665_1670352_-1563062 type=barren mass=0.0036920729492058825 radius=0.217838595089208 gravity=8 pressure=0 tempK=476 oxygen=false locked=true rings=false rotation=17309 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1834664_1670352_-1563056 -1834674_1670352_-1563044 type=barren mass=0.0070392049784330805 radius=0.2706016219811198 gravity=10 pressure=0 tempK=303 oxygen=false locked=false rings=false rotation=58582 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1834664_1670352_-1563056 -1834697_1670352_-1563082 type=greenhouse mass=5.306353329769671 radius=1.5301172792246798 gravity=227 pressure=1600 tempK=304 oxygen=false locked=false rings=false rotation=12411 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1834664_1670352_-1563056 -1834844_1670355_-1562989 type=barren mass=0.04424796580738948 radius=0.4169178260700219 gravity=25 pressure=15 tempK=86 oxygen=false locked=false rings=false rotation=73360 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1834664_1670352_-1563056 -1835637_1670333_-1563103 type=icegiant mass=77.12815588696303 radius=5.941666373737659 gravity=218 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=13678 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2208092_-1790276_1685270 -2208092_-1790276_1685270 type=barren mass=0.005648418871458735 radius=0.2385909118845583 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=24775 metallicity=1.5155492707302036 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2290821_1403075_856935 -2290821_1403075_856935 type=ice mass=0.266707898716306 radius=0.6825852386298956 gravity=57 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=81338 metallicity=1.294876211758993 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3014847_-1420592_5288623 -3014847_-1420592_5288623 type=ice mass=0.005044358396498096 radius=0.2259652104199226 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=62265 metallicity=1.1198703399625591 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3374315_4976731_3972240 -3374315_4976731_3972240 type=ice mass=2.0498293005739696 radius=1.257168129941409 gravity=130 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=44853 metallicity=1.1089654140853202 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -436060_-2366191_-949510 -435942_-2366191_-949421 type=icegiant mass=246.21329912216967 radius=9.841958949883894 gravity=254 pressure=1600 tempK=125 oxygen=false locked=false rings=false rotation=4994 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -436060_-2366191_-949510 -436000_-2366193_-949507 type=ice mass=0.0025001436401144083 radius=0.20969431093726135 gravity=6 pressure=0 tempK=82 oxygen=false locked=false rings=false rotation=57151 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -436060_-2366191_-949510 -436025_-2366191_-949523 type=superearth mass=11.986869338529015 radius=1.9765339194370526 gravity=307 pressure=1600 tempK=268 oxygen=false locked=false rings=false rotation=40867 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -436060_-2366191_-949510 -436055_-2366191_-949514 type=desert mass=0.01731826282819392 radius=0.34512975441876653 gravity=15 pressure=0 tempK=289 oxygen=false locked=true rings=false rotation=51106 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -436060_-2366191_-949510 -436060_-2366191_-949510 type=lava mass=0.09081514066754195 radius=0.5369742020541469 gravity=31 pressure=0 tempK=1794 oxygen=false locked=true rings=false rotation=73337 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -436060_-2366191_-949510 -436062_-2366191_-949508 type=desert mass=1.1028863386362178 radius=0.98242013651816 gravity=114 pressure=64 tempK=470 oxygen=false locked=true rings=false rotation=58647 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -436060_-2366191_-949510 -436065_-2366191_-949500 type=greenhouse mass=2.5515226570009824 radius=1.334015205340586 gravity=143 pressure=991 tempK=330 oxygen=false locked=false rings=false rotation=63087 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -528291_6799917_1589264 -528281_6799917_1589255 type=greenhouse mass=4.411463451533969 radius=1.549060693570877 gravity=184 pressure=569 tempK=435 oxygen=false locked=false rings=false rotation=26130 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -528291_6799917_1589264 -528289_6799918_1589188 type=exotic mass=2.524783156469872 radius=1.2598310614277537 gravity=159 pressure=1600 tempK=302 oxygen=false locked=false rings=false rotation=24005 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -528291_6799917_1589264 -528291_6799917_1589264 type=lava mass=2.0192563355271664 radius=1.2060277237340613 gravity=139 pressure=2 tempK=2886 oxygen=false locked=true rings=false rotation=61672 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -528291_6799917_1589264 -528415_6799932_1589887 type=superearth mass=9.60122918654861 radius=1.9904254713501455 gravity=242 pressure=1600 tempK=104 oxygen=false locked=false rings=false rotation=18054 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -528291_6799917_1589264 -529048_6799943_1588586 type=gasgiant mass=102.21010191973816 radius=6.715430513074007 gravity=227 pressure=1600 tempK=76 oxygen=false locked=false rings=false rotation=5319 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -935767_4549081_-1731362 -935767_4549081_-1731362 type=ice mass=0.0028984057910889116 radius=0.21212769702860157 gravity=6 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=10990 metallicity=1.4772416207188779 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1683940_-183048_-1702546 1683940_-183048_-1702546 type=barren mass=0.010671389149843244 radius=0.28622983833858173 gravity=13 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=18839 metallicity=1.0655911026250537 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1920315_1641073_-2834001 1920315_1641073_-2834001 type=ice mass=0.10126275801176421 radius=0.5693690184380397 gravity=31 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=7760 metallicity=0.5352071006919491 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 233560_2922383_5010954 233560_2922383_5010954 type=superearth mass=4.212603209068318 radius=1.4350700127224734 gravity=205 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=20455 metallicity=1.2849716814294743 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 236691_5203788_1778447 236688_5203788_1778436 type=gasgiant mass=193.7262334816812 radius=8.86769297367945 gravity=246 pressure=1600 tempK=281 oxygen=false locked=false rings=true rotation=7749 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 236691_5203788_1778447 236690_5203788_1778446 type=lava mass=6.252037775157124 radius=1.5952759531425913 gravity=246 pressure=1600 tempK=802 oxygen=false locked=true rings=false rotation=87775 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 236691_5203788_1778447 236691_5203788_1778447 type=barren mass=0.05717937002910721 radius=0.4520758203563616 gravity=28 pressure=0 tempK=999 oxygen=false locked=true rings=false rotation=79433 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 236691_5203788_1778447 236693_5203788_1778447 type=barren mass=0.17809896703041364 radius=0.6434024890394139 gravity=43 pressure=2 tempK=291 oxygen=false locked=true rings=false rotation=28497 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 236691_5203788_1778447 236832_5203788_1778492 type=ice mass=10.574660573064731 radius=1.924690506299482 gravity=285 pressure=1600 tempK=80 oxygen=false locked=false rings=false rotation=35653 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2374829_-1121220_6754955 2369207_-1121220_6758455 type=icegiant mass=138.84327717357925 radius=7.672085602961861 gravity=236 pressure=1600 tempK=10 oxygen=false locked=false rings=true rotation=11389 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2374829_-1121220_6754955 2374772_-1121218_6754973 type=gasgiant mass=23.639860574930704 radius=3.553184930957361 gravity=187 pressure=1600 tempK=99 oxygen=false locked=false rings=false rotation=12132 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2374829_-1121220_6754955 2374790_-1121224_6755042 type=barren mass=0.004459332908709616 radius=0.23724534463610833 gravity=8 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=91424 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2374829_-1121220_6754955 2374827_-1121220_6754949 type=barren mass=0.13337484033974645 radius=0.5970847496460991 gravity=37 pressure=18 tempK=158 oxygen=false locked=true rings=false rotation=18630 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2374829_-1121220_6754955 2374829_-1121220_6754955 type=lava mass=14.647744112503442 radius=2.073549074884492 gravity=341 pressure=1075 tempK=1869 oxygen=false locked=true rings=false rotation=19737 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2374829_-1121220_6754955 2374830_-1121220_6754957 type=barren mass=0.002441342699551333 radius=0.2038047543227626 gravity=6 pressure=0 tempK=275 oxygen=false locked=true rings=false rotation=78813 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2374829_-1121220_6754955 2374835_-1121220_6754946 type=gasgiant mass=278.8962302254018 radius=10.39003058667359 gravity=258 pressure=1600 tempK=228 oxygen=false locked=false rings=false rotation=5158 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3353092_-2509636_325419 3353092_-2509636_325419 type=barren mass=0.06282138922649873 radius=0.5025877735087989 gravity=25 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=16027 metallicity=1.2404503262924214 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3392849_5972712_-904179 3392849_5972712_-904179 type=barren mass=0.009876212507077011 radius=0.27812854586282826 gravity=13 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=54299 metallicity=1.4252753140160968 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4690385_6611748_3834194 4690385_6611748_3834194 type=ice mass=0.0023842033479558543 radius=0.20246189913346935 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=9734 metallicity=0.41246374291164867 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4725059_6081849_-1115135 4725059_6081849_-1115135 type=ice mass=0.034968096229707576 radius=0.4079580640952251 gravity=21 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=45324 metallicity=0.9010689721776831 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5148034_-2724950_2998726 5147687_-2724953_2998875 type=gasgiant mass=152.70280703610828 radius=7.996125437815539 gravity=239 pressure=1600 tempK=132 oxygen=false locked=false rings=true rotation=7410 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5148034_-2724950_2998726 5147811_-2724969_2998165 type=barren mass=0.002992299640949069 radius=0.21770853518619304 gravity=6 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=20666 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5148034_-2724950_2998726 5147837_-2724951_2998798 type=ice mass=5.466740675672476 radius=1.6094577312496248 gravity=211 pressure=1600 tempK=167 oxygen=false locked=false rings=false rotation=6715 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5148034_-2724950_2998726 5148028_-2724950_2998708 type=barren mass=0.0041496893892041375 radius=0.22373207447041826 gravity=8 pressure=0 tempK=301 oxygen=false locked=false rings=false rotation=17131 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5148034_-2724950_2998726 5148030_-2724949_2998788 type=ice mass=0.36690758271858726 radius=0.7535567326131476 gravity=65 pressure=102 tempK=154 oxygen=false locked=false rings=false rotation=27675 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5148034_-2724950_2998726 5148034_-2724950_2998726 type=lava mass=3.417268629173799 radius=1.3915138241071623 gravity=176 pressure=3 tempK=3058 oxygen=false locked=true rings=false rotation=37045 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5549745_1612567_-2459150 5549745_1612567_-2459150 type=ice mass=1.050098511379873 radius=1.0153534844490044 gravity=102 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=24753 metallicity=0.3928649683191534 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5569465_-2990317_6697828 5569089_-2990307_6697897 type=ice mass=16.479662665760813 radius=2.113428500370335 gravity=369 pressure=1600 tempK=84 oxygen=false locked=false rings=false rotation=9024 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5569465_-2990317_6697828 5569364_-2990322_6697902 type=icegiant mass=43.02187167996671 radius=4.609784144078134 gravity=202 pressure=1600 tempK=156 oxygen=false locked=false rings=true rotation=12742 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5569465_-2990317_6697828 5569437_-2990317_6697833 type=gasgiant mass=234.4117416739366 radius=9.634001360033889 gravity=253 pressure=1600 tempK=330 oxygen=false locked=false rings=true rotation=9011 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5569465_-2990317_6697828 5569462_-2990317_6697826 type=barren mass=0.37214398915860636 radius=0.8179661755366368 gravity=56 pressure=7 tempK=474 oxygen=false locked=true rings=false rotation=80736 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5569465_-2990317_6697828 5569465_-2990317_6697828 type=lava mass=0.0021659933087211126 radius=0.2020085491799058 gravity=5 pressure=0 tempK=2067 oxygen=false locked=true rings=false rotation=23645 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5569465_-2990317_6697828 5569476_-2990318_6697814 type=superearth mass=18.11852475983095 radius=2.1269076094125596 gravity=400 pressure=1600 tempK=452 oxygen=false locked=false rings=false rotation=11585 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5569465_-2990317_6697828 5569481_-2990317_6697828 type=ocean mass=1.059189868092209 radius=0.9864908144267657 gravity=109 pressure=211 tempK=310 oxygen=false locked=false rings=false rotation=60621 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5569465_-2990317_6697828 5569514_-2990315_6697863 type=ice mass=0.02160095503404406 radius=0.35129067425550137 gravity=18 pressure=1 tempK=94 oxygen=false locked=false rings=false rotation=45792 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5569465_-2990317_6697828 5570051_-2990300_6698005 type=superearth mass=16.306973848511685 radius=2.212745593215032 gravity=333 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=56640 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6255448_968120_6199733 6255448_968120_6199733 type=ice mass=0.013200272332713349 radius=0.29383472367066443 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=43984 metallicity=0.8253735417868293 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6513943_-990357_-607133 6513943_-990357_-607133 type=barren mass=0.09201947656728872 radius=0.5014717372599318 gravity=37 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=42447 metallicity=1.334877107093964 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544001_1191037_686867 type=ice mass=0.5984193937280294 radius=0.8686309348655668 gravity=79 pressure=1600 tempK=65 oxygen=false locked=false rings=false rotation=54198 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544042_1191031_686755 type=barren mass=0.018747194391930556 radius=0.3603369419189195 gravity=14 pressure=3 tempK=44 oxygen=false locked=false rings=false rotation=23367 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544063_1191032_686756 type=gasgiant mass=269.96545473267946 radius=10.24404348301498 gravity=257 pressure=1600 tempK=97 oxygen=false locked=false rings=true rotation=4821 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544127_1191032_686779 type=ice mass=0.0028044798010789514 radius=0.21819393982079735 gravity=6 pressure=0 tempK=101 oxygen=false locked=false rings=false rotation=6542 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544128_1191032_686758 type=barren mass=0.02158702650110108 radius=0.34477288327841055 gravity=18 pressure=3 tempK=87 oxygen=false locked=false rings=false rotation=20290 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544134_1191033_686798 type=barren mass=0.006171595858463438 radius=0.2431769234018869 gravity=10 pressure=0 tempK=109 oxygen=false locked=false rings=false rotation=85791 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544135_1191032_686776 type=gasgiant mass=64.28754965431646 radius=5.489376420198329 gravity=213 pressure=1600 tempK=311 oxygen=false locked=false rings=true rotation=8173 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544137_1191032_686776 type=gasgiant mass=63.106403051729444 radius=5.445296272973473 gravity=213 pressure=1600 tempK=350 oxygen=false locked=false rings=true rotation=10319 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544139_1191031_686761 type=barren mass=0.003125528676288277 radius=0.22450064981738688 gravity=6 pressure=0 tempK=99 oxygen=false locked=false rings=false rotation=9194 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544139_1191032_686779 type=superearth mass=9.957185823292782 radius=1.8148705728399028 gravity=302 pressure=1600 tempK=555 oxygen=false locked=true rings=false rotation=15904 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544139_1191032_686783 type=superearth mass=9.204815331500313 radius=1.7790736666585254 gravity=291 pressure=1600 tempK=786 oxygen=false locked=true rings=false rotation=16380 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544140_1191032_686782 type=lava mass=0.003495511330012961 radius=0.23077661816220355 gravity=7 pressure=0 tempK=1052 oxygen=false locked=true rings=false rotation=25695 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544141_1191032_686779 type=superearth mass=11.047638995008654 radius=1.9621716147310968 gravity=287 pressure=1600 tempK=524 oxygen=false locked=true rings=false rotation=14669 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544141_1191032_686782 type=superearth mass=11.967664795083397 radius=2.0090225676405997 gravity=297 pressure=1600 tempK=840 oxygen=false locked=true rings=false rotation=15220 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544141_1191032_686784 type=greenhouse mass=7.79141122491771 radius=1.7709663722662257 gravity=248 pressure=1600 tempK=459 oxygen=false locked=true rings=true rotation=10842 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544142_1191032_686781 type=lava mass=12.653119261304946 radius=1.9402070673972158 gravity=336 pressure=1600 tempK=713 oxygen=false locked=true rings=false rotation=11634 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544142_1191032_686785 type=superearth mass=4.019480587927113 radius=1.4212622127384715 gravity=199 pressure=1600 tempK=485 oxygen=false locked=true rings=false rotation=24322 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544145_1191032_686780 type=desert mass=0.546198035443908 radius=0.8401221729861281 gravity=77 pressure=156 tempK=234 oxygen=false locked=true rings=false rotation=31071 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544146_1191032_686775 type=superearth mass=20.051271375975926 radius=2.1414268787063544 gravity=400 pressure=1600 tempK=308 oxygen=false locked=false rings=false rotation=11752 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544158_1191032_686742 type=ice mass=1.0540998936610393 radius=1.0270189716791964 gravity=100 pressure=1600 tempK=126 oxygen=false locked=false rings=false rotation=13739 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544172_1191032_686773 type=ice mass=0.514057046161867 radius=0.8104176080813175 gravity=78 pressure=768 tempK=121 oxygen=false locked=false rings=true rotation=12337 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544189_1191032_686805 type=gasgiant mass=282.3938328204548 radius=10.446483309221662 gravity=259 pressure=1600 tempK=120 oxygen=false locked=false rings=false rotation=12786 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6544140_1191032_686782 6544204_1191032_686802 type=ice mass=0.003386499072493738 radius=0.20723184818615456 gravity=8 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=36624 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6799915_4871887_422782 6799897_4871887_422780 type=icegiant mass=236.86012225174596 radius=9.677622955960413 gravity=253 pressure=1600 tempK=179 oxygen=false locked=false rings=true rotation=7323 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6799915_4871887_422782 6799906_4871885_422865 type=ice mass=0.21716084645281308 radius=0.6802498305881435 gravity=47 pressure=180 tempK=45 oxygen=false locked=false rings=false rotation=69571 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6799915_4871887_422782 6799913_4871887_422790 type=gasgiant mass=230.7374117373395 radius=9.568051622775512 gravity=252 pressure=1600 tempK=266 oxygen=false locked=false rings=true rotation=5781 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6799915_4871887_422782 6799914_4871887_422781 type=greenhouse mass=1.371342956962792 radius=1.103751530221875 gravity=113 pressure=245 tempK=347 oxygen=false locked=true rings=false rotation=10002 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6799915_4871887_422782 6799915_4871887_422782 type=lava mass=10.748140594691607 radius=2.042247167653855 gravity=258 pressure=160 tempK=1139 oxygen=false locked=true rings=false rotation=30465 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6799915_4871887_422782 6799916_4871887_422785 type=greenhouse mass=22.039018182220467 radius=2.4101333906778977 gravity=379 pressure=1600 tempK=328 oxygen=false locked=true rings=false rotation=13224 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6799915_4871887_422782 6799919_4871887_422780 type=ice mass=0.02972947091143899 radius=0.40514559833386565 gravity=18 pressure=1 tempK=153 oxygen=false locked=true rings=false rotation=65714 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6799915_4871887_422782 6799949_4871888_422822 type=barren mass=0.0028085340043301683 radius=0.2023591304452867 gravity=7 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=9454 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 772836_5636740_4927735 772836_5636740_4927735 type=ice mass=0.5889054698003752 radius=0.8207995700352966 gravity=87 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=56192 metallicity=0.5085707964711265 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 907102_1605031_1799741 907102_1605031_1799741 type=ice mass=20.825124090311277 radius=2.2676109846054535 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=36240 metallicity=0.47844268929701317 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1080086_1555356_4409113 id=-1337008461 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless + system -1834664_1670352_-1563056 id=-1729416869 kind=STAR name=PGS--3525313.0.-3525313 starTemp=100 starSize=1.0540038347244263 + system -2208092_-1790276_1685270 id=-1765959065 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless + system -2290821_1403075_856935 id=-1992245269 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless + system -3014847_-1420592_5288623 id=-794196001 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless + system -3374315_4976731_3972240 id=-1160817649 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system -436060_-2366191_-949510 id=-498839401 kind=STAR name=PGS--3525313.-3525313.-3525313 starTemp=70 starSize=0.8857885003089905 + system -528291_6799917_1589264 id=-1828554265 kind=STAR name=PGS--3525313.3525313.0 starTemp=100 starSize=1.122846007347107 + system -935767_4549081_-1731362 id=-1358211061 kind=ROGUE_PLANET name=PGR--3525313.3525313.-3525313 starless + system 1683940_-183048_-1702546 id=-167956389 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless + system 1920315_1641073_-2834001 id=-1131707005 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless + system 233560_2922383_5010954 id=-1390378301 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless + system 236691_5203788_1778447 id=-1521889893 kind=STAR name=PGS-0.3525313.0 starTemp=40 starSize=0.8503480553627014 + system 2374829_-1121220_6754955 id=-1897301649 kind=STAR name=PGS-0.-3525313.3525313 starTemp=40 starSize=0.709514319896698 + system 3353092_-2509636_325419 id=-1078102009 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless + system 3392849_5972712_-904179 id=-1470468277 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless + system 4690385_6611748_3834194 id=-284738901 kind=ROGUE_PLANET name=PGR-3525313.3525313.3525313 starless + system 4725059_6081849_-1115135 id=-28341585 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless + system 5148034_-2724950_2998726 id=-258191809 kind=STAR name=PGS-3525313.-3525313.0 starTemp=100 starSize=1.2604737281799316 + system 5549745_1612567_-2459150 id=-910886457 kind=ROGUE_PLANET name=PGR-3525313.0.-3525313 starless + system 5569465_-2990317_6697828 id=-1028453113 kind=STAR name=PGS-3525313.-3525313.3525313 starTemp=70 starSize=1.1755964756011963 + system 6255448_968120_6199733 id=-886206973 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless + system 6513943_-990357_-607133 id=-207608917 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless + system 6544140_1191032_686782 id=-1188842121 kind=STAR name=PGS-3525313.0.0 starTemp=40 starSize=0.9326234459877014 + system 6799915_4871887_422782 id=-15767913 kind=STAR name=PGS-3525313.3525313.0 starTemp=40 starSize=0.6828064918518066 + system 772836_5636740_4927735 id=-1020620017 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless + system 907102_1605031_1799741 id=-1806957165 kind=ROGUE_PLANET name=PGR-0.0.0 starless seed 6942069 systems=27 - body -2000631_2857747_461230 -2000631_2857747_461230 kind=ROGUE_PLANET orbit=0 radius=1.1175634439669084 starId=-205872669 frame=true - body -2314717_-1231317_1000094 -2314717_-1231317_1000094 kind=MOON orbit=0 radius=0.20000204191531482 starId=-901558001 frame=false - body -2314717_-1231317_1000094 -2314717_-1231317_1000094 kind=MOON orbit=0 radius=0.21789599586602817 starId=-901558001 frame=false - body -2314717_-1231317_1000094 -2314717_-1231317_1000094 kind=ROGUE_PLANET orbit=0 radius=1.863005654483607 starId=-901558001 frame=true - body -2877371_7494376_5224403 -2877371_7494376_5224403 kind=ROGUE_PLANET orbit=0 radius=1.2717789680091072 starId=-665523365 frame=true - body -2895205_-1927811_6875549 -2895205_-1927811_6875549 kind=ROGUE_PLANET orbit=0 radius=2.3163732961564074 starId=-909247865 frame=true - body -4275006_509094_-3579362 -4262355_509628_-3582487 kind=ASTEROID_BELT orbit=69744 radius=0.0 starId=-1541462001 frame=true - body -4275006_509094_-3579362 -4268441_508768_-3574542 kind=PLANET orbit=43590 radius=0.8647633663246215 starId=-1541462001 frame=true - body -4275006_509094_-3579362 -4274966_509095_-3579392 kind=PLANET orbit=267 radius=0.737582062833023 starId=-1541462001 frame=true - body -4275006_509094_-3579362 -4274993_509128_-3578183 kind=ASTEROID_BELT orbit=6308 radius=0.0 starId=-1541462001 frame=true - body -4275006_509094_-3579362 -4275006_509094_-3579362 kind=STAR orbit=0 radius=0.0 starId=-1541462001 frame=true - body -4275006_509094_-3579362 -4275120_509094_-3579130 kind=STAR orbit=1380 radius=82.15348955512047 starId=-1541462002 frame=true - body -4275006_509094_-3579362 -4275443_509018_-3582785 kind=MOON orbit=18458 radius=0.2802839163886913 starId=-1541462001 frame=false - body -4275006_509094_-3579362 -4275443_509018_-3582785 kind=PLANET orbit=18458 radius=1.5665193433650293 starId=-1541462001 frame=true - body -4275006_509094_-3579362 -4275958_509131_-3579524 kind=PLANET orbit=5167 radius=2.212651053453857 starId=-1541462001 frame=true - body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=GAS_GIANT orbit=11356 radius=6.011853442836375 starId=-1541462001 frame=true - body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=MOON orbit=11356 radius=0.49926944261156775 starId=-1541462001 frame=false - body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=MOON orbit=11356 radius=0.6192696527926773 starId=-1541462001 frame=false - body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=MOON orbit=11356 radius=0.6743314451130911 starId=-1541462001 frame=false - body -4275006_509094_-3579362 -4276233_509117_-3581095 kind=MOON orbit=11356 radius=0.7322653156046546 starId=-1541462001 frame=false - body -4438016_6980803_4442099 -4437971_6980802_4441977 kind=GAS_GIANT orbit=697 radius=5.847417597949033 starId=-1046829965 frame=true - body -4438016_6980803_4442099 -4438012_6980803_4442118 kind=PLANET orbit=103 radius=1.6973042599502073 starId=-1046829965 frame=true - body -4438016_6980803_4442099 -4438016_6980803_4442099 kind=STAR orbit=0 radius=0.0 starId=-1046829965 frame=true - body -4438016_6980803_4442099 -4438019_6980803_4442098 kind=PLANET orbit=16 radius=1.7455655341946663 starId=-1046829965 frame=true - body -4438016_6980803_4442099 -4438081_6980802_4442067 kind=ASTEROID_BELT orbit=387 radius=0.0 starId=-1046829965 frame=true - body -4438016_6980803_4442099 -4438219_6980809_4442053 kind=ASTEROID_BELT orbit=1115 radius=0.0 starId=-1046829965 frame=true - body -454681_4814105_7133477 -454681_4814105_7133477 kind=MOON orbit=0 radius=0.8248445313980841 starId=-331820225 frame=false - body -454681_4814105_7133477 -454681_4814105_7133477 kind=ROGUE_PLANET orbit=0 radius=0.6183832792523067 starId=-331820225 frame=true - body -4597348_-2289120_-3893104 -4597348_-2289120_-3893104 kind=ROGUE_PLANET orbit=0 radius=0.8002391142960747 starId=-45556669 frame=true - body -4680832_8971816_-4852244 -4680832_8971816_-4852244 kind=MOON orbit=0 radius=0.8726694211157258 starId=-800100513 frame=false - body -4680832_8971816_-4852244 -4680832_8971816_-4852244 kind=MOON orbit=0 radius=1.8437515181150548 starId=-800100513 frame=false - body -4680832_8971816_-4852244 -4680832_8971816_-4852244 kind=ROGUE_PLANET orbit=0 radius=0.24455038227961948 starId=-800100513 frame=true - body 1167088_-1577312_6736547 1167088_-1577312_6736547 kind=MOON orbit=0 radius=2.4810088471172254 starId=-41953553 frame=false - body 1167088_-1577312_6736547 1167088_-1577312_6736547 kind=ROGUE_PLANET orbit=0 radius=2.308617529166714 starId=-41953553 frame=true - body 1436889_2367423_-1041444 1436889_2367423_-1041444 kind=ROGUE_PLANET orbit=0 radius=0.4647793875769384 starId=-718399965 frame=true - body 1474783_6118657_-2639989 1474783_6118657_-2639989 kind=ROGUE_PLANET orbit=0 radius=0.21285660343752053 starId=-27450993 frame=true - body 167759_-751993_-3497839 167759_-751993_-3497839 kind=MOON orbit=0 radius=1.653297304016185 starId=-771118101 frame=false - body 167759_-751993_-3497839 167759_-751993_-3497839 kind=ROGUE_PLANET orbit=0 radius=1.7397452680925143 starId=-771118101 frame=true - body 441136_1799137_1921288 441136_1799137_1921288 kind=MOON orbit=0 radius=7.563308321550477 starId=-1525225641 frame=false - body 441136_1799137_1921288 441136_1799137_1921288 kind=ROGUE_PLANET orbit=0 radius=0.20117409820683874 starId=-1525225641 frame=true - body 4829162_6045847_8018026 4829126_6045849_8018058 kind=ASTEROID_BELT orbit=260 radius=0.0 starId=-51688385 frame=true - body 4829162_6045847_8018026 4829158_6045847_8018023 kind=PLANET orbit=24 radius=2.350205838409168 starId=-51688385 frame=true - body 4829162_6045847_8018026 4829160_6045847_8018021 kind=ASTEROID_BELT orbit=31 radius=0.0 starId=-51688385 frame=true - body 4829162_6045847_8018026 4829161_6045847_8018016 kind=GAS_GIANT orbit=56 radius=3.0667534125078753 starId=-51688385 frame=true - body 4829162_6045847_8018026 4829162_6045847_8018026 kind=STAR orbit=0 radius=0.0 starId=-51688385 frame=true - body 4829162_6045847_8018026 4829163_6045847_8018027 kind=PLANET orbit=8 radius=0.6618193175012526 starId=-51688385 frame=true - body 4829162_6045847_8018026 4829180_6045847_8018001 kind=MOON orbit=163 radius=0.20488986961295125 starId=-51688385 frame=false - body 4829162_6045847_8018026 4829180_6045847_8018001 kind=MOON orbit=163 radius=0.24662987357862132 starId=-51688385 frame=false - body 4829162_6045847_8018026 4829180_6045847_8018001 kind=PLANET orbit=163 radius=0.48927571213765514 starId=-51688385 frame=true - body 4829162_6045847_8018026 4829282_6045847_8018043 kind=STAR orbit=650 radius=85.98566706061364 starId=-51688386 frame=true - body 4853201_4468647_7791279 4853201_4468647_7791279 kind=MOON orbit=0 radius=2.338014275612157 starId=-1963232729 frame=false - body 4853201_4468647_7791279 4853201_4468647_7791279 kind=ROGUE_PLANET orbit=0 radius=0.4239783392591294 starId=-1963232729 frame=true - body 5345275_3039615_-4867366 5345275_3039615_-4867366 kind=ROGUE_PLANET orbit=0 radius=0.32781097335099096 starId=-1930757837 frame=true - body 5791350_-2733430_-4256275 5791350_-2733430_-4256275 kind=ROGUE_PLANET orbit=0 radius=0.21174735932880662 starId=-220927317 frame=true - body 587917_5522727_989590 587829_5522722_989492 kind=ASTEROID_BELT orbit=705 radius=0.0 starId=-1767151601 frame=true - body 587917_5522727_989590 587859_5522730_989564 kind=PLANET orbit=338 radius=1.928779571339509 starId=-1767151601 frame=true - body 587917_5522727_989590 587863_5522723_989652 kind=GAS_GIANT orbit=441 radius=9.769767771696792 starId=-1767151601 frame=true - body 587917_5522727_989590 587863_5522723_989652 kind=MOON orbit=441 radius=0.2174686697377889 starId=-1767151601 frame=false - body 587917_5522727_989590 587863_5522723_989652 kind=MOON orbit=441 radius=0.515587549611561 starId=-1767151601 frame=false - body 587917_5522727_989590 587905_5522727_989595 kind=MOON orbit=71 radius=0.3394053532009783 starId=-1767151601 frame=false - body 587917_5522727_989590 587905_5522727_989595 kind=MOON orbit=71 radius=0.7446296027401826 starId=-1767151601 frame=false - body 587917_5522727_989590 587905_5522727_989595 kind=PLANET orbit=71 radius=0.3658287705174149 starId=-1767151601 frame=true - body 587917_5522727_989590 587910_5522727_989607 kind=GAS_GIANT orbit=100 radius=8.196656972278635 starId=-1767151601 frame=true - body 587917_5522727_989590 587910_5522727_989607 kind=MOON orbit=100 radius=0.22159543085238867 starId=-1767151601 frame=false - body 587917_5522727_989590 587917_5522727_989588 kind=MOON orbit=11 radius=0.22325450226540466 starId=-1767151601 frame=false - body 587917_5522727_989590 587917_5522727_989588 kind=PLANET orbit=11 radius=2.257547577904001 starId=-1767151601 frame=true - body 587917_5522727_989590 587917_5522727_989590 kind=STAR orbit=0 radius=0.0 starId=-1767151601 frame=true - body 587917_5522727_989590 587918_5522727_989590 kind=MOON orbit=6 radius=0.2586921589715278 starId=-1767151601 frame=false - body 587917_5522727_989590 587918_5522727_989590 kind=PLANET orbit=6 radius=0.788616566373262 starId=-1767151601 frame=true - body 587917_5522727_989590 587921_5522727_989589 kind=PLANET orbit=20 radius=1.6748013792552296 starId=-1767151601 frame=true - body 587917_5522727_989590 587923_5522727_989581 kind=ASTEROID_BELT orbit=55 radius=0.0 starId=-1767151601 frame=true - body 587917_5522727_989590 587923_5522727_989594 kind=PLANET orbit=40 radius=1.4511788031234742 starId=-1767151601 frame=true - body 587917_5522727_989590 587947_5522728_989592 kind=GAS_GIANT orbit=160 radius=7.300658322926931 starId=-1767151601 frame=true - body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.2067364242405062 starId=-1767151601 frame=false - body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.23017884841460212 starId=-1767151601 frame=false - body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.2948552526319255 starId=-1767151601 frame=false - body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.3089593434746649 starId=-1767151601 frame=false - body 587917_5522727_989590 587947_5522728_989592 kind=MOON orbit=160 radius=0.3592246315698835 starId=-1767151601 frame=false - body 7060560_5700377_2876755 7060560_5700377_2876755 kind=ROGUE_PLANET orbit=0 radius=0.2718294017783941 starId=-1316506565 frame=true - body 7136101_9473497_8932593 7136101_9473497_8932593 kind=ROGUE_PLANET orbit=0 radius=0.5764923072666365 starId=-723847149 frame=true - body 7364543_-1483056_2565191 7364543_-1483056_2565191 kind=MOON orbit=0 radius=0.2429303883510497 starId=-1945655921 frame=false - body 7364543_-1483056_2565191 7364543_-1483056_2565191 kind=MOON orbit=0 radius=0.8234012503125645 starId=-1945655921 frame=false - body 7364543_-1483056_2565191 7364543_-1483056_2565191 kind=ROGUE_PLANET orbit=0 radius=1.815800241268268 starId=-1945655921 frame=true - body 7828323_3394113_830032 7828323_3394113_830032 kind=MOON orbit=0 radius=2.1984708188976825 starId=-784249741 frame=false - body 7828323_3394113_830032 7828323_3394113_830032 kind=ROGUE_PLANET orbit=0 radius=1.6484804785844318 starId=-784249741 frame=true - body 7974653_6760692_-2065905 7974653_6760692_-2065905 kind=ROGUE_PLANET orbit=0 radius=1.9935622970096312 starId=-1716020649 frame=true - body 7996706_-2186877_7355245 7969279_-2186877_7337231 kind=STAR orbit=175478 radius=92.08370618999004 starId=-1661134222 frame=true - body 7996706_-2186877_7355245 7996538_-2186870_7355360 kind=GAS_GIANT orbit=1090 radius=5.784037934580867 starId=-1661134221 frame=true - body 7996706_-2186877_7355245 7996596_-2186880_7355218 kind=ASTEROID_BELT orbit=605 radius=0.0 starId=-1661134221 frame=true - body 7996706_-2186877_7355245 7996683_-2186878_7355223 kind=MOON orbit=170 radius=0.2437557445272609 starId=-1661134221 frame=false - body 7996706_-2186877_7355245 7996683_-2186878_7355223 kind=MOON orbit=170 radius=0.6245520811489388 starId=-1661134221 frame=false - body 7996706_-2186877_7355245 7996683_-2186878_7355223 kind=PLANET orbit=170 radius=1.2202907808972563 starId=-1661134221 frame=true - body 7996706_-2186877_7355245 7996704_-2186877_7355240 kind=STAR orbit=28 radius=92.08370618999004 starId=-1661134223 frame=true - body 7996706_-2186877_7355245 7996706_-2186877_7355245 kind=STAR orbit=0 radius=0.0 starId=-1661134221 frame=true - body 7996706_-2186877_7355245 7996851_-2186877_7355537 kind=ASTEROID_BELT orbit=1744 radius=0.0 starId=-1661134221 frame=true - body 8321778_4704939_9248429 8321469_4704913_9249045 kind=MOON orbit=3687 radius=0.5305367936750038 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8321469_4704913_9249045 kind=MOON orbit=3687 radius=0.7109215379700333 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8321469_4704913_9249045 kind=PLANET orbit=3687 radius=0.45245851529142395 starId=-1361770949 frame=true - body 8321778_4704939_9248429 8321730_4704942_9248455 kind=ASTEROID_BELT orbit=292 radius=0.0 starId=-1361770949 frame=true - body 8321778_4704939_9248429 8321731_4704936_9248343 kind=GAS_GIANT orbit=526 radius=10.158618618611886 starId=-1361770949 frame=true - body 8321778_4704939_9248429 8321731_4704936_9248343 kind=MOON orbit=526 radius=0.21078927632064628 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8321731_4704936_9248343 kind=MOON orbit=526 radius=0.3533663064113992 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8321731_4704936_9248343 kind=MOON orbit=526 radius=0.39397170175713786 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8321731_4704936_9248343 kind=MOON orbit=526 radius=0.45392051937072936 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8321778_4704939_9248429 kind=STAR orbit=0 radius=0.0 starId=-1361770949 frame=true - body 8321778_4704939_9248429 8321781_4704939_9248425 kind=PLANET orbit=28 radius=1.2131860370084657 starId=-1361770949 frame=true - body 8321778_4704939_9248429 8321787_4704939_9248459 kind=STAR orbit=169 radius=76.91228431642055 starId=-1361770950 frame=true - body 8321778_4704939_9248429 8321848_4704948_9248254 kind=GAS_GIANT orbit=1012 radius=7.297168858591538 starId=-1361770949 frame=true - body 8321778_4704939_9248429 8321848_4704948_9248254 kind=MOON orbit=1012 radius=0.21183434315225672 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8321848_4704948_9248254 kind=MOON orbit=1012 radius=0.3052065627341558 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8321848_4704948_9248254 kind=MOON orbit=1012 radius=0.4885069925715317 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8321848_4704948_9248254 kind=MOON orbit=1012 radius=0.7013628705572932 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8322209_4704920_9248345 kind=MOON orbit=2350 radius=0.39830164316357086 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8322209_4704920_9248345 kind=MOON orbit=2350 radius=0.5521362719668897 starId=-1361770949 frame=false - body 8321778_4704939_9248429 8322209_4704920_9248345 kind=PLANET orbit=2350 radius=2.278982012939558 starId=-1361770949 frame=true - body 8321778_4704939_9248429 8322380_4704929_9249353 kind=ASTEROID_BELT orbit=5899 radius=0.0 starId=-1361770949 frame=true - body 958466_-2560139_3404636 958466_-2560139_3404636 kind=ROGUE_PLANET orbit=0 radius=0.4161581934278241 starId=-680806101 frame=true - derived -2000631_2857747_461230 -2000631_2857747_461230 type=ice mass=1.5509619296421744 radius=1.1175634439669084 gravity=124 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=47972 metallicity=0.577565904469171 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2314717_-1231317_1000094 -2314717_-1231317_1000094 type=superearth mass=7.692509079840432 radius=1.863005654483607 gravity=222 pressure=0 tempK=43 oxygen=false locked=false rings=false rotation=11824 metallicity=0.7141346578312251 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2877371_7494376_5224403 -2877371_7494376_5224403 type=ice mass=2.691093337632512 radius=1.2717789680091072 gravity=166 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=7094 metallicity=0.581091632464666 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2895205_-1927811_6875549 -2895205_-1927811_6875549 type=superearth mass=19.592550392607542 radius=2.3163732961564074 gravity=365 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=27177 metallicity=1.4608463176547102 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4275006_509094_-3579362 -4262355_509628_-3582487 type=barren mass=0.006232896285339044 radius=0.2619277258550008 gravity=9 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=63382 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4275006_509094_-3579362 -4268441_508768_-3574542 type=ice mass=0.6003813645154001 radius=0.8647633663246215 gravity=80 pressure=1600 tempK=80 oxygen=false locked=false rings=false rotation=10347 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4275006_509094_-3579362 -4274966_509095_-3579392 type=desert mass=0.3928649259378479 radius=0.737582062833023 gravity=72 pressure=1 tempK=528 oxygen=false locked=false rings=false rotation=11734 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4275006_509094_-3579362 -4274993_509128_-3578183 type=icegiant mass=94.73366886910857 radius=6.4972655072397725 gravity=224 pressure=1600 tempK=225 oxygen=false locked=false rings=false rotation=5925 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4275006_509094_-3579362 -4275006_509094_-3579362 type=unclassified mass=0.006157838693660983 radius=0.24531213715311925 gravity=10 pressure=0 tempK=8640 oxygen=false locked=true rings=true rotation=9756 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4275006_509094_-3579362 -4275120_509094_-3579130 type=superearth mass=30.459553437561752 radius=2.379810241823191 gravity=400 pressure=1600 tempK=523 oxygen=false locked=false rings=false rotation=30850 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4275006_509094_-3579362 -4275443_509018_-3582785 type=superearth mass=3.952703328435687 radius=1.5665193433650293 gravity=161 pressure=1600 tempK=143 oxygen=false locked=false rings=false rotation=15199 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4275006_509094_-3579362 -4275958_509131_-3579524 type=superearth mass=16.210601325628947 radius=2.212651053453857 gravity=331 pressure=1600 tempK=270 oxygen=false locked=false rings=false rotation=8326 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4275006_509094_-3579362 -4276233_509117_-3581095 type=icegiant mass=79.23977417178959 radius=6.011853442836375 gravity=219 pressure=1600 tempK=167 oxygen=false locked=false rings=true rotation=13874 metallicity=0.9189752427445081 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4438016_6980803_4442099 -4437971_6980802_4441977 type=gasgiant mass=74.34322522188954 radius=5.847417597949033 gravity=217 pressure=1600 tempK=132 oxygen=false locked=false rings=false rotation=5180 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4438016_6980803_4442099 -4438012_6980803_4442118 type=greenhouse mass=5.634910161229004 radius=1.6973042599502073 gravity=196 pressure=1600 tempK=289 oxygen=false locked=false rings=false rotation=11508 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4438016_6980803_4442099 -4438016_6980803_4442099 type=lava mass=0.06323371623274694 radius=0.473488411952418 gravity=28 pressure=0 tempK=1796 oxygen=false locked=true rings=false rotation=52867 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4438016_6980803_4442099 -4438019_6980803_4442098 type=lava mass=6.6904109023411795 radius=1.7455655341946663 gravity=220 pressure=1600 tempK=1010 oxygen=false locked=true rings=false rotation=20793 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4438016_6980803_4442099 -4438081_6980802_4442067 type=icegiant mass=241.47156099019836 radius=9.759095883804656 gravity=254 pressure=1600 tempK=177 oxygen=false locked=false rings=true rotation=6705 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4438016_6980803_4442099 -4438219_6980809_4442053 type=barren mass=0.0031404818084259196 radius=0.20917351228651077 gravity=7 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=19110 metallicity=0.41665454288682024 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -454681_4814105_7133477 -454681_4814105_7133477 type=ice mass=0.17426138830929258 radius=0.6183832792523067 gravity=46 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=43883 metallicity=0.4230696455081826 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4597348_-2289120_-3893104 -4597348_-2289120_-3893104 type=barren mass=0.4565249679975668 radius=0.8002391142960747 gravity=71 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=8054 metallicity=0.4627221934143715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4680832_8971816_-4852244 -4680832_8971816_-4852244 type=barren mass=0.0062750825875540935 radius=0.24455038227961948 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=32229 metallicity=0.980274814369234 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1167088_-1577312_6736547 1167088_-1577312_6736547 type=superearth mass=24.21660930929242 radius=2.308617529166714 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=78771 metallicity=0.9850103985780603 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1436889_2367423_-1041444 1436889_2367423_-1041444 type=barren mass=0.04740156517764068 radius=0.4647793875769384 gravity=22 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=43740 metallicity=0.9490739923459447 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1474783_6118657_-2639989 1474783_6118657_-2639989 type=ice mass=0.0029047379942924965 radius=0.21285660343752053 gravity=6 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=44596 metallicity=0.40640924213177304 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 167759_-751993_-3497839 167759_-751993_-3497839 type=superearth mass=9.343148962232025 radius=1.7397452680925143 gravity=309 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=36699 metallicity=0.5144347213851472 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 441136_1799137_1921288 441136_1799137_1921288 type=barren mass=0.0022266009259117106 radius=0.20117409820683874 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=29510 metallicity=0.3831595348763942 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4829162_6045847_8018026 4829126_6045849_8018058 type=ice mass=2.1029791267430484 radius=1.2481868701659535 gravity=135 pressure=1600 tempK=113 oxygen=false locked=false rings=false rotation=17414 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4829162_6045847_8018026 4829158_6045847_8018023 type=superearth mass=22.189971166285517 radius=2.350205838409168 gravity=400 pressure=1600 tempK=417 oxygen=false locked=true rings=false rotation=9760 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4829162_6045847_8018026 4829160_6045847_8018021 type=exotic mass=1.0231194073980834 radius=1.0063670398132052 gravity=101 pressure=808 tempK=309 oxygen=false locked=true rings=false rotation=9403 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4829162_6045847_8018026 4829161_6045847_8018016 type=gasgiant mass=16.84943395728299 radius=3.0667534125078753 gravity=179 pressure=1600 tempK=251 oxygen=false locked=false rings=true rotation=9958 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4829162_6045847_8018026 4829162_6045847_8018026 type=barren mass=0.2022866885608661 radius=0.6454013420135354 gravity=49 pressure=0 tempK=961 oxygen=false locked=true rings=false rotation=32184 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4829162_6045847_8018026 4829163_6045847_8018027 type=barren mass=0.2682804120014818 radius=0.6618193175012526 gravity=61 pressure=12 tempK=340 oxygen=false locked=true rings=false rotation=90507 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4829162_6045847_8018026 4829180_6045847_8018001 type=ice mass=0.07539902914771969 radius=0.48927571213765514 gravity=31 pressure=54 tempK=62 oxygen=false locked=false rings=false rotation=94182 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4829162_6045847_8018026 4829282_6045847_8018043 type=gasgiant mass=79.50637799485442 radius=6.020639444361429 gravity=219 pressure=1600 tempK=81 oxygen=false locked=false rings=true rotation=9564 metallicity=0.39684990237458684 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4853201_4468647_7791279 4853201_4468647_7791279 type=ice mass=0.039769133653525045 radius=0.4239783392591294 gravity=22 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=16003 metallicity=0.8985853797134048 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5345275_3039615_-4867366 5345275_3039615_-4867366 type=barren mass=0.015090364122544584 radius=0.32781097335099096 gravity=14 pressure=0 tempK=21 oxygen=false locked=false rings=true rotation=9560 metallicity=0.6592137837908945 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5791350_-2733430_-4256275 5791350_-2733430_-4256275 type=barren mass=0.00356415955964074 radius=0.21174735932880662 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=true rotation=26620 metallicity=0.7832763078453421 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587829_5522722_989492 type=barren mass=0.03176468238245332 radius=0.37276832630008583 gravity=23 pressure=9 tempK=38 oxygen=false locked=false rings=false rotation=71896 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587859_5522730_989564 type=superearth mass=9.13029145017862 radius=1.928779571339509 gravity=245 pressure=1600 tempK=118 oxygen=false locked=false rings=false rotation=39692 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587863_5522723_989652 type=icegiant mass=242.0793237896986 radius=9.769767771696792 gravity=254 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=6186 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587905_5522727_989595 type=ice mass=0.02260281926731236 radius=0.3658287705174149 gravity=17 pressure=1 tempK=100 oxygen=false locked=false rings=false rotation=81831 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587910_5522727_989607 type=gasgiant mass=161.6547584471927 radius=8.196656972278635 gravity=241 pressure=1600 tempK=200 oxygen=false locked=false rings=true rotation=13770 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587917_5522727_989588 type=greenhouse mass=24.61686910908349 radius=2.257547577904001 gravity=400 pressure=1600 tempK=508 oxygen=false locked=true rings=false rotation=8592 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587917_5522727_989590 type=lava mass=23.773968929357295 radius=2.2377199011218356 gravity=400 pressure=1297 tempK=2204 oxygen=false locked=true rings=false rotation=8466 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587918_5522727_989590 type=desert mass=0.4919682647497833 radius=0.788616566373262 gravity=79 pressure=10 tempK=395 oxygen=false locked=true rings=false rotation=12150 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587921_5522727_989589 type=greenhouse mass=6.746705939385756 radius=1.6748013792552296 gravity=241 pressure=1600 tempK=377 oxygen=false locked=true rings=false rotation=39618 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587923_5522727_989581 type=superearth mass=3.982003394899363 radius=1.5415096258089278 gravity=168 pressure=1600 tempK=294 oxygen=false locked=false rings=false rotation=19367 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587923_5522727_989594 type=exotic mass=3.1161263559236354 radius=1.4511788031234742 gravity=148 pressure=1600 tempK=344 oxygen=false locked=true rings=false rotation=17339 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 587917_5522727_989590 587947_5522728_989592 type=gasgiant mass=123.86727922451554 radius=7.300658322926931 gravity=232 pressure=1600 tempK=158 oxygen=false locked=false rings=true rotation=6307 metallicity=0.7350222294247728 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7060560_5700377_2876755 7060560_5700377_2876755 type=barren mass=0.0075290595549173354 radius=0.2718294017783941 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=64530 metallicity=1.0669737181725631 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7136101_9473497_8932593 7136101_9473497_8932593 type=ice mass=0.16003853224505066 radius=0.5764923072666365 gravity=48 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=36716 metallicity=0.9058761263463274 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7364543_-1483056_2565191 7364543_-1483056_2565191 type=ice mass=8.68456268430672 radius=1.815800241268268 gravity=263 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=84169 metallicity=1.451623064986201 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7828323_3394113_830032 7828323_3394113_830032 type=superearth mass=5.581741802538662 radius=1.6484804785844318 gravity=205 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=12703 metallicity=0.9653087518188345 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7974653_6760692_-2065905 7974653_6760692_-2065905 type=ice mass=12.126402985250099 radius=1.9935622970096312 gravity=305 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=7264 metallicity=0.5593650899132638 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7996706_-2186877_7355245 7969279_-2186877_7337231 type=icegiant mass=66.51326254565343 radius=5.5712121763523825 gravity=214 pressure=1600 tempK=8 oxygen=false locked=false rings=true rotation=6163 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7996706_-2186877_7355245 7996538_-2186870_7355360 type=gasgiant mass=72.50292856904187 radius=5.784037934580867 gravity=217 pressure=1600 tempK=105 oxygen=false locked=false rings=false rotation=6439 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7996706_-2186877_7355245 7996596_-2186880_7355218 type=gasgiant mass=20.021182674614654 radius=3.3055672195119294 gravity=183 pressure=1600 tempK=141 oxygen=false locked=false rings=false rotation=10316 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7996706_-2186877_7355245 7996683_-2186878_7355223 type=superearth mass=2.556340141638674 radius=1.2202907808972563 gravity=172 pressure=1600 tempK=289 oxygen=false locked=false rings=false rotation=9626 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7996706_-2186877_7355245 7996704_-2186877_7355240 type=superearth mass=6.2774689588736585 radius=1.55198833221646 gravity=261 pressure=1600 tempK=617 oxygen=false locked=true rings=false rotation=6209 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7996706_-2186877_7355245 7996706_-2186877_7355245 type=barren mass=0.07531318336790613 radius=0.5091277160105288 gravity=29 pressure=0 tempK=998 oxygen=false locked=true rings=false rotation=77118 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7996706_-2186877_7355245 7996851_-2186877_7355537 type=gasgiant mass=85.27436538634228 radius=6.206792068644254 gravity=221 pressure=1600 tempK=83 oxygen=false locked=false rings=false rotation=8495 metallicity=0.6757436536220521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8321778_4704939_9248429 8321469_4704913_9249045 type=barren mass=0.04124148346273014 radius=0.45245851529142395 gravity=20 pressure=18 tempK=49 oxygen=false locked=false rings=false rotation=15807 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8321778_4704939_9248429 8321730_4704942_9248455 type=ice mass=0.15832531713214607 radius=0.5991106795837686 gravity=44 pressure=26 tempK=143 oxygen=false locked=false rings=false rotation=6147 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8321778_4704939_9248429 8321731_4704936_9248343 type=gasgiant mass=264.81565349914257 radius=10.158618618611886 gravity=257 pressure=1600 tempK=253 oxygen=false locked=false rings=true rotation=6413 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8321778_4704939_9248429 8321778_4704939_9248429 type=lava mass=0.05077807073042751 radius=0.4307541898393471 gravity=27 pressure=0 tempK=2991 oxygen=false locked=true rings=false rotation=18596 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8321778_4704939_9248429 8321781_4704939_9248425 type=desert mass=1.7610003129621679 radius=1.2131860370084657 gravity=120 pressure=50 tempK=530 oxygen=false locked=true rings=false rotation=82167 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8321778_4704939_9248429 8321787_4704939_9248459 type=greenhouse mass=22.321865746529177 radius=2.376303730221804 gravity=395 pressure=1600 tempK=376 oxygen=false locked=false rings=false rotation=14594 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8321778_4704939_9248429 8321848_4704948_9248254 type=gasgiant mass=123.73115160984537 radius=7.297168858591538 gravity=232 pressure=1600 tempK=183 oxygen=false locked=false rings=false rotation=6866 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8321778_4704939_9248429 8322209_4704920_9248345 type=superearth mass=16.336557863330242 radius=2.278982012939558 gravity=315 pressure=1600 tempK=130 oxygen=false locked=false rings=false rotation=20488 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8321778_4704939_9248429 8322380_4704929_9249353 type=ice mass=13.082720853039595 radius=2.0303380169775687 gravity=317 pressure=1600 tempK=71 oxygen=false locked=false rings=false rotation=76471 metallicity=0.6213515438012032 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 958466_-2560139_3404636 958466_-2560139_3404636 type=barren mass=0.03893878624401527 radius=0.4161581934278241 gravity=22 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=34432 metallicity=0.6902859630758016 terrain=TerrainOption[NATIVE genType=0 w=1] - system -2000631_2857747_461230 id=-205872669 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless - system -2314717_-1231317_1000094 id=-901558001 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless - system -2877371_7494376_5224403 id=-665523365 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless - system -2895205_-1927811_6875549 id=-909247865 kind=ROGUE_PLANET name=PGR--5002361.-5002361.5002361 starless - system -4275006_509094_-3579362 id=-1541462001 kind=STAR name=PGS--5002361.0.-5002361 starTemp=220 starSize=2.3564164638519287 - system -4438016_6980803_4442099 id=-1046829965 kind=STAR name=PGS--5002361.5002361.0 starTemp=70 starSize=0.8875247836112976 - system -454681_4814105_7133477 id=-331820225 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless - system -4597348_-2289120_-3893104 id=-45556669 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless - system -4680832_8971816_-4852244 id=-800100513 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless - system 1167088_-1577312_6736547 id=-41953553 kind=ROGUE_PLANET name=PGR-0.-5002361.5002361 starless - system 1436889_2367423_-1041444 id=-718399965 kind=ROGUE_PLANET name=PGR-0.0.-5002361 starless - system 1474783_6118657_-2639989 id=-27450993 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless - system 167759_-751993_-3497839 id=-771118101 kind=ROGUE_PLANET name=PGR-0.-5002361.-5002361 starless - system 441136_1799137_1921288 id=-1525225641 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 4829162_6045847_8018026 id=-51688385 kind=STAR name=PGS-0.5002361.5002361 starTemp=40 starSize=0.7876309156417847 - system 4853201_4468647_7791279 id=-1963232729 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless - system 5345275_3039615_-4867366 id=-1930757837 kind=ROGUE_PLANET name=PGR-5002361.0.-5002361 starless - system 5791350_-2733430_-4256275 id=-220927317 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless - system 587917_5522727_989590 id=-1767151601 kind=STAR name=PGS-0.5002361.0 starTemp=40 starSize=0.8977140188217163 - system 7060560_5700377_2876755 id=-1316506565 kind=ROGUE_PLANET name=PGR-5002361.5002361.0 starless - system 7136101_9473497_8932593 id=-723847149 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless - system 7364543_-1483056_2565191 id=-1945655921 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless - system 7828323_3394113_830032 id=-784249741 kind=ROGUE_PLANET name=PGR-5002361.0.0 starless - system 7974653_6760692_-2065905 id=-1716020649 kind=ROGUE_PLANET name=PGR-5002361.5002361.-5002361 starless - system 7996706_-2186877_7355245 id=-1661134221 kind=STAR name=PGS-5002361.-5002361.5002361 starTemp=40 starSize=0.8434891104698181 - system 8321778_4704939_9248429 id=-1361770949 kind=STAR name=PGS-5002361.0.5002361 starTemp=100 starSize=1.2055790424346924 - system 958466_-2560139_3404636 id=-680806101 kind=ROGUE_PLANET name=PGR-0.-5002361.0 starless + body -1295452_590737_6017888 -1295070_590746_6017369 kind=MOON orbit=3445 radius=0.3190080862601766 starId=-871681409 frame=false at=-138529,0,-254546 + body -1295452_590737_6017888 -1295070_590746_6017369 kind=MOON orbit=3445 radius=0.7116873727132678 starId=-871681409 frame=false at=120234,0,261922 + body -1295452_590737_6017888 -1295070_590746_6017369 kind=PLANET orbit=3445 radius=1.2070218778244706 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295073_590747_6019489 kind=GAS_GIANT orbit=8799 radius=6.682294021731574 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295073_590747_6019489 kind=MOON orbit=8799 radius=0.20311680067120372 starId=-871681409 frame=false at=-1297409,0,451118 + body -1295452_590737_6017888 -1295073_590747_6019489 kind=MOON orbit=8799 radius=0.41603813684826363 starId=-871681409 frame=false at=-1143764,0,-1222873 + body -1295452_590737_6017888 -1295073_590747_6019489 kind=MOON orbit=8799 radius=0.6870083437381647 starId=-871681409 frame=false at=-296041,0,-393218 + body -1295452_590737_6017888 -1295358_590738_6017838 kind=ASTEROID_BELT orbit=571 radius=0.0 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295387_590731_6018000 kind=MOON orbit=693 radius=0.3784651847896745 starId=-871681409 frame=false at=254807,0,116075 + body -1295452_590737_6017888 -1295387_590731_6018000 kind=MOON orbit=693 radius=0.6783165555531443 starId=-871681409 frame=false at=-509904,0,204543 + body -1295452_590737_6017888 -1295387_590731_6018000 kind=PLANET orbit=693 radius=2.2787722785946882 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295442_590736_6017862 kind=PLANET orbit=149 radius=1.5837503051367467 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295451_590737_6017973 kind=PLANET orbit=452 radius=1.123888274347984 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295452_590737_6017888 kind=STAR orbit=0 radius=0.0 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295454_590737_6017903 kind=PLANET orbit=83 radius=0.20116599292739867 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295493_590722_6018208 kind=GAS_GIANT orbit=1727 radius=7.507047019723977 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295493_590722_6018208 kind=MOON orbit=1727 radius=0.31306945689191784 starId=-871681409 frame=false at=-1019710,0,874287 + body -1295452_590737_6017888 -1295496_590735_6017878 kind=PLANET orbit=242 radius=0.7098331686844603 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295585_590736_6017749 kind=GAS_GIANT orbit=1028 radius=10.409352129035774 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295585_590736_6017749 kind=MOON orbit=1028 radius=0.25777039740738394 starId=-871681409 frame=false at=107673,0,799986 + body -1295452_590737_6017888 -1295812_590729_6018754 kind=MOON orbit=5017 radius=0.27760010462297624 starId=-871681409 frame=false at=-269255,0,-120037 + body -1295452_590737_6017888 -1295812_590729_6018754 kind=MOON orbit=5017 radius=0.5125251114922812 starId=-871681409 frame=false at=138754,0,220117 + body -1295452_590737_6017888 -1295812_590729_6018754 kind=PLANET orbit=5017 radius=1.0093703974830894 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1295861_590671_6015628 kind=PLANET orbit=12287 radius=0.9155336635218376 starId=-871681409 frame=true at=0,0,0 + body -1295452_590737_6017888 -1299084_590613_6018440 kind=ASTEROID_BELT orbit=19659 radius=0.0 starId=-871681409 frame=true at=0,0,0 + body -1420862_99037_1766933 -1420862_99037_1766933 kind=MOON orbit=0 radius=0.49299935448250815 starId=-280597797 frame=false at=57791,0,9701 + body -1420862_99037_1766933 -1420862_99037_1766933 kind=MOON orbit=0 radius=1.4713603847399748 starId=-280597797 frame=false at=45046,0,138246 + body -1420862_99037_1766933 -1420862_99037_1766933 kind=ROGUE_PLANET orbit=0 radius=0.5046332779050597 starId=-280597797 frame=true at=0,0,0 + body -1488035_-3034313_-3156807 -1488035_-3034313_-3156807 kind=ROGUE_PLANET orbit=0 radius=1.0104407160721844 starId=-401453429 frame=true at=0,0,0 + body -2011949_3789060_5390006 -2011949_3789060_5390006 kind=ROGUE_PLANET orbit=0 radius=1.8294486117039206 starId=-66168209 frame=true at=0,0,0 + body -268539_1650411_-2625440 -268539_1650411_-2625440 kind=ROGUE_PLANET orbit=0 radius=0.2601853496316905 starId=-1871662501 frame=true at=0,0,0 + body -3063801_-759422_5862386 -3063743_-759422_5862350 kind=ASTEROID_BELT orbit=366 radius=0.0 starId=-1332799893 frame=true at=0,0,0 + body -3063801_-759422_5862386 -3063792_-759422_5862390 kind=MOON orbit=53 radius=0.574243829678742 starId=-1332799893 frame=false at=-2228,0,-34929 + body -3063801_-759422_5862386 -3063792_-759422_5862390 kind=MOON orbit=53 radius=0.6652106597419126 starId=-1332799893 frame=false at=7281,0,96125 + body -3063801_-759422_5862386 -3063792_-759422_5862390 kind=PLANET orbit=53 radius=0.38115810884355894 starId=-1332799893 frame=true at=0,0,0 + body -3063801_-759422_5862386 -3063799_-759422_5862382 kind=PLANET orbit=22 radius=1.6780172812581946 starId=-1332799893 frame=true at=0,0,0 + body -3063801_-759422_5862386 -3063801_-759422_5862386 kind=STAR orbit=0 radius=0.0 starId=-1332799893 frame=true at=0,0,0 + body -3063801_-759422_5862386 -3063803_-759422_5862385 kind=MOON orbit=11 radius=0.2000098685870336 starId=-1332799893 frame=false at=-83170,0,16546 + body -3063801_-759422_5862386 -3063803_-759422_5862385 kind=PLANET orbit=11 radius=0.5061482545056208 starId=-1332799893 frame=true at=0,0,0 + body -3063801_-759422_5862386 -3063812_-759422_5862406 kind=PLANET orbit=121 radius=0.3235908932389939 starId=-1332799893 frame=true at=0,0,0 + body -3063801_-759422_5862386 -3063836_-759422_5862361 kind=PLANET orbit=229 radius=1.7102798695984978 starId=-1332799893 frame=true at=0,0,0 + body -3063801_-759422_5862386 -3063922_-759422_5862448 kind=STAR orbit=728 radius=88.8110494530201 starId=-1332799894 frame=true at=0,0,0 + body -3359999_5029417_-1201971 -3359999_5029417_-1201971 kind=ROGUE_PLANET orbit=0 radius=0.2756640180131294 starId=-63962517 frame=true at=0,0,0 + body -464282_-3293100_220531 -464282_-3293100_220531 kind=ROGUE_PLANET orbit=0 radius=1.7196633145190408 starId=-1130433613 frame=true at=0,0,0 + body -589874_5099752_2648961 -589845_5099756_2648834 kind=ASTEROID_BELT orbit=699 radius=0.0 starId=-573199273 frame=true at=0,0,0 + body -589874_5099752_2648961 -589846_5099755_2649038 kind=PLANET orbit=437 radius=1.3085576757295254 starId=-573199273 frame=true at=0,0,0 + body -589874_5099752_2648961 -589856_5099752_2648936 kind=GAS_GIANT orbit=164 radius=7.982010427523395 starId=-573199273 frame=true at=0,0,0 + body -589874_5099752_2648961 -589856_5099752_2648936 kind=MOON orbit=164 radius=0.593769818824629 starId=-573199273 frame=false at=639497,0,-548651 + body -589874_5099752_2648961 -589872_5099751_2648944 kind=ASTEROID_BELT orbit=91 radius=0.0 starId=-573199273 frame=true at=0,0,0 + body -589874_5099752_2648961 -589874_5099752_2648960 kind=PLANET orbit=8 radius=0.4372934321766312 starId=-573199273 frame=true at=0,0,0 + body -589874_5099752_2648961 -589874_5099752_2648961 kind=STAR orbit=0 radius=0.0 starId=-573199273 frame=true at=0,0,0 + body -589874_5099752_2648961 -589875_5099752_2648965 kind=PLANET orbit=23 radius=0.9102675836184433 starId=-573199273 frame=true at=0,0,0 + body -589874_5099752_2648961 -589876_5099752_2648973 kind=PLANET orbit=66 radius=0.5502112106561361 starId=-573199273 frame=true at=0,0,0 + body -589874_5099752_2648961 -590614_5099752_2650343 kind=STAR orbit=8383 radius=88.68083709418774 starId=-573199274 frame=true at=0,0,0 + body 115515_4884922_-1449848 115515_4884922_-1449848 kind=ROGUE_PLANET orbit=0 radius=2.182709991409553 starId=-1386063681 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1339940_-2645564_6560548 kind=ASTEROID_BELT orbit=625 radius=0.0 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340044_-2645561_6560536 kind=GAS_GIANT orbit=135 radius=4.438597591383325 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340052_-2645562_6560553 kind=MOON orbit=34 radius=0.37367358790291005 starId=-1921583641 frame=false at=66587,0,-1326 + body 1340056_-2645562_6560558 1340052_-2645562_6560553 kind=PLANET orbit=34 radius=0.27373337680420534 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340053_-2645562_6560556 kind=PLANET orbit=18 radius=1.6681649801111824 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340056_-2645562_6560558 kind=STAR orbit=0 radius=0.0 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340057_-2645562_6560559 kind=PLANET orbit=7 radius=0.22651086590250133 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340058_-2645562_6560557 kind=MOON orbit=11 radius=0.2027678761726554 starId=-1921583641 frame=false at=-22054,0,18521 + body 1340056_-2645562_6560558 1340058_-2645562_6560557 kind=MOON orbit=11 radius=0.736864067127678 starId=-1921583641 frame=false at=16307,0,73001 + body 1340056_-2645562_6560558 1340058_-2645562_6560557 kind=PLANET orbit=11 radius=0.2522099109479354 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340061_-2645562_6560554 kind=ASTEROID_BELT orbit=34 radius=0.0 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=GAS_GIANT orbit=62 radius=6.729055091962263 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=MOON orbit=62 radius=0.21906139343685416 starId=-1921583641 frame=false at=812207,0,555496 + body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=MOON orbit=62 radius=0.31490040064715435 starId=-1921583641 frame=false at=361247,0,754142 + body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=MOON orbit=62 radius=0.44318684402677405 starId=-1921583641 frame=false at=-843448,0,509904 + body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=MOON orbit=62 radius=0.5348797147409434 starId=-1921583641 frame=false at=-946432,0,941537 + body 1340056_-2645562_6560558 1340073_-2645562_6560550 kind=PLANET orbit=102 radius=1.1216746451827144 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340102_-2645562_6560536 kind=MOON orbit=273 radius=0.21610846864650235 starId=-1921583641 frame=false at=23894,0,-2257 + body 1340056_-2645562_6560558 1340102_-2645562_6560536 kind=PLANET orbit=273 radius=0.26426323362501386 starId=-1921583641 frame=true at=0,0,0 + body 1340056_-2645562_6560558 1340119_-2645561_6560521 kind=MOON orbit=391 radius=0.2095120881778693 starId=-1921583641 frame=false at=-201046,0,-312517 + body 1340056_-2645562_6560558 1340119_-2645561_6560521 kind=MOON orbit=391 radius=0.5144584346323176 starId=-1921583641 frame=false at=-15563,0,221053 + body 1340056_-2645562_6560558 1340119_-2645561_6560521 kind=PLANET orbit=391 radius=2.198756624518602 starId=-1921583641 frame=true at=0,0,0 + body 136345_2380618_4435608 136345_2380618_4435608 kind=MOON orbit=0 radius=0.32784282519564134 starId=-655881041 frame=false at=158566,0,-34143 + body 136345_2380618_4435608 136345_2380618_4435608 kind=MOON orbit=0 radius=0.4451578677661996 starId=-655881041 frame=false at=-160991,0,1706 + body 136345_2380618_4435608 136345_2380618_4435608 kind=ROGUE_PLANET orbit=0 radius=0.5726161013605666 starId=-655881041 frame=true at=0,0,0 + body 2294785_6087239_1852785 2294785_6087239_1852785 kind=ROGUE_PLANET orbit=0 radius=0.3788569313823317 starId=-747899749 frame=true at=0,0,0 + body 2324700_3395074_-773152 2324700_3395074_-773152 kind=MOON orbit=0 radius=1.5614013723674798 starId=-333451093 frame=false at=-38138,0,-81078 + body 2324700_3395074_-773152 2324700_3395074_-773152 kind=ROGUE_PLANET orbit=0 radius=0.32820893486546776 starId=-333451093 frame=true at=0,0,0 + body 2380220_-2985326_2261989 2380220_-2985326_2261989 kind=MOON orbit=0 radius=2.2315871511185916 starId=-846895665 frame=false at=-107775,0,-93683 + body 2380220_-2985326_2261989 2380220_-2985326_2261989 kind=ROGUE_PLANET orbit=0 radius=0.8176934272940033 starId=-846895665 frame=true at=0,0,0 + body 2455719_-1575919_-1684641 2455719_-1575919_-1684641 kind=MOON orbit=0 radius=0.24593919185767682 starId=-1093289653 frame=false at=19349,0,-21869 + body 2455719_-1575919_-1684641 2455719_-1575919_-1684641 kind=MOON orbit=0 radius=1.4677044102869057 starId=-1093289653 frame=false at=35895,0,38722 + body 2455719_-1575919_-1684641 2455719_-1575919_-1684641 kind=ROGUE_PLANET orbit=0 radius=0.23495795814274667 starId=-1093289653 frame=true at=0,0,0 + body 4041882_708425_6811475 4041882_708425_6811475 kind=MOON orbit=0 radius=0.7690545770847357 starId=-1527098829 frame=false at=159065,0,-248917 + body 4041882_708425_6811475 4041882_708425_6811475 kind=ROGUE_PLANET orbit=0 radius=1.029137364803036 starId=-1527098829 frame=true at=0,0,0 + body 4374645_5543557_6609018 4374645_5543557_6609018 kind=ROGUE_PLANET orbit=0 radius=1.0349140608384135 starId=-138926061 frame=true at=0,0,0 + body 4760881_3403866_-1378830 4760881_3403866_-1378830 kind=MOON orbit=0 radius=0.947747417710435 starId=-1839995337 frame=false at=273547,0,-11762 + body 4760881_3403866_-1378830 4760881_3403866_-1378830 kind=ROGUE_PLANET orbit=0 radius=1.244696857260704 starId=-1839995337 frame=true at=0,0,0 + body 5532167_-2797664_-2258523 5532167_-2797664_-2258523 kind=MOON orbit=0 radius=1.1673146084339747 starId=-850423465 frame=false at=10209,0,24779 + body 5532167_-2797664_-2258523 5532167_-2797664_-2258523 kind=ROGUE_PLANET orbit=0 radius=0.41943619401321175 starId=-850423465 frame=true at=0,0,0 + body 5628766_5108790_338559 5628766_5108790_338559 kind=MOON orbit=0 radius=0.3769649461819978 starId=-1665662161 frame=false at=-34548,0,-49544 + body 5628766_5108790_338559 5628766_5108790_338559 kind=MOON orbit=0 radius=1.7503827766419675 starId=-1665662161 frame=false at=-76783,0,-17714 + body 5628766_5108790_338559 5628766_5108790_338559 kind=ROGUE_PLANET orbit=0 radius=0.28277463573335193 starId=-1665662161 frame=true at=0,0,0 + body 5670369_-3422417_6764355 5670369_-3422417_6764355 kind=ROGUE_PLANET orbit=0 radius=1.0232019832097992 starId=-862267757 frame=true at=0,0,0 + body 5876998_3296879_3027393 5876998_3296879_3027393 kind=ROGUE_PLANET orbit=0 radius=2.0861890597953177 starId=-286545925 frame=true at=0,0,0 + body 6068647_-3169217_818787 6068647_-3169217_818787 kind=MOON orbit=0 radius=0.6312789796589846 starId=-621289557 frame=false at=-3640,0,121746 + body 6068647_-3169217_818787 6068647_-3169217_818787 kind=MOON orbit=0 radius=0.7024140531407117 starId=-621289557 frame=false at=45735,0,-64415 + body 6068647_-3169217_818787 6068647_-3169217_818787 kind=ROGUE_PLANET orbit=0 radius=0.4506215156332336 starId=-621289557 frame=true at=0,0,0 + body 668050_2680028_1601017 668050_2680028_1601017 kind=ROGUE_PLANET orbit=0 radius=1.3571907745157803 starId=-1525225641 frame=true at=0,0,0 + body 6948671_5049241_-2964475 6948671_5049241_-2964475 kind=MOON orbit=0 radius=0.7110288498579667 starId=-162398185 frame=false at=-41898,0,82130 + body 6948671_5049241_-2964475 6948671_5049241_-2964475 kind=ROGUE_PLANET orbit=0 radius=0.3116116058746792 starId=-162398185 frame=true at=0,0,0 + body 728798_4100023_3876685 728752_4100025_3876660 kind=GAS_GIANT orbit=278 radius=9.366458180023432 starId=-1903899713 frame=true at=0,0,0 + body 728798_4100023_3876685 728752_4100025_3876660 kind=MOON orbit=278 radius=0.2017256452460188 starId=-1903899713 frame=false at=-497064,0,-1468560 + body 728798_4100023_3876685 728752_4100025_3876660 kind=MOON orbit=278 radius=0.30964028113932174 starId=-1903899713 frame=false at=-1615396,0,-1813716 + body 728798_4100023_3876685 728752_4100025_3876660 kind=MOON orbit=278 radius=0.3987516690238629 starId=-1903899713 frame=false at=-1087247,0,-1632238 + body 728798_4100023_3876685 728752_4100025_3876660 kind=MOON orbit=278 radius=0.5538235729148501 starId=-1903899713 frame=false at=-1196641,0,1314230 + body 728798_4100023_3876685 728795_4100023_3876676 kind=GAS_GIANT orbit=52 radius=7.051145770054424 starId=-1903899713 frame=true at=0,0,0 + body 728798_4100023_3876685 728795_4100023_3876676 kind=MOON orbit=52 radius=0.21599279470357996 starId=-1903899713 frame=false at=-511636,0,-207734 + body 728798_4100023_3876685 728795_4100023_3876676 kind=MOON orbit=52 radius=0.3824736496878405 starId=-1903899713 frame=false at=-1026229,0,-1490958 + body 728798_4100023_3876685 728795_4100023_3876676 kind=MOON orbit=52 radius=0.5086753545023199 starId=-1903899713 frame=false at=473884,0,-213115 + body 728798_4100023_3876685 728798_4100023_3876682 kind=MOON orbit=14 radius=0.24619612572853286 starId=-1903899713 frame=false at=-272907,0,-251322 + body 728798_4100023_3876685 728798_4100023_3876682 kind=MOON orbit=14 radius=0.37447857405867035 starId=-1903899713 frame=false at=-104608,0,274760 + body 728798_4100023_3876685 728798_4100023_3876682 kind=PLANET orbit=14 radius=1.81070986321312 starId=-1903899713 frame=true at=0,0,0 + body 728798_4100023_3876685 728798_4100023_3876685 kind=STAR orbit=0 radius=0.0 starId=-1903899713 frame=true at=0,0,0 + body 728798_4100023_3876685 728802_4100023_3876688 kind=ASTEROID_BELT orbit=28 radius=0.0 starId=-1903899713 frame=true at=0,0,0 + body 728798_4100023_3876685 728826_4100019_3876763 kind=ASTEROID_BELT orbit=444 radius=0.0 starId=-1903899713 frame=true at=0,0,0 + derived -1295452_590737_6017888 -1295070_590746_6017369 type=superearth mass=2.3671386651819746 radius=1.2070218778244706 gravity=162 pressure=1600 tempK=189 oxygen=false locked=false rings=false rotation=12841 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295073_590747_6019489 type=gasgiant mass=101.05383037726808 radius=6.682294021731574 gravity=226 pressure=1600 tempK=108 oxygen=false locked=false rings=true rotation=5771 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295358_590738_6017838 type=superearth mass=8.602401240662678 radius=1.7689241251777645 gravity=275 pressure=1600 tempK=465 oxygen=false locked=false rings=false rotation=15646 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295387_590731_6018000 type=greenhouse mass=19.216360137189277 radius=2.2787722785946882 gravity=370 pressure=1600 tempK=326 oxygen=false locked=false rings=false rotation=79585 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295442_590736_6017862 type=lava mass=5.4132542997814035 radius=1.5837503051367467 gravity=216 pressure=1600 tempK=969 oxygen=false locked=false rings=false rotation=25383 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295451_590737_6017973 type=desert mass=1.258015523087503 radius=1.123888274347984 gravity=100 pressure=181 tempK=303 oxygen=false locked=false rings=false rotation=40088 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295452_590737_6017888 type=lava mass=0.01666233092074892 radius=0.33359015825984484 gravity=15 pressure=0 tempK=5260 oxygen=false locked=true rings=true rotation=31042 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295454_590737_6017903 type=barren mass=0.002928379387151399 radius=0.20116599292739867 gravity=7 pressure=0 tempK=574 oxygen=false locked=false rings=false rotation=30799 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295493_590722_6018208 type=gasgiant mass=132.06962874363816 radius=7.507047019723977 gravity=234 pressure=1600 tempK=245 oxygen=false locked=false rings=true rotation=7629 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295496_590735_6017878 type=desert mass=0.3300211540029742 radius=0.7098331686844603 gravity=65 pressure=3 tempK=317 oxygen=false locked=false rings=false rotation=22990 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295585_590736_6017749 type=gasgiant mass=280.0905487859904 radius=10.409352129035774 gravity=258 pressure=1600 tempK=318 oxygen=false locked=false rings=false rotation=4895 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295812_590729_6018754 type=ice mass=0.8308521390281213 radius=1.0093703974830894 gravity=82 pressure=1600 tempK=136 oxygen=false locked=false rings=false rotation=6508 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1295861_590671_6015628 type=ice mass=0.7054345416786596 radius=0.9155336635218376 gravity=84 pressure=1068 tempK=78 oxygen=false locked=false rings=false rotation=40584 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1295452_590737_6017888 -1299084_590613_6018440 type=ice mass=1.6788893782433592 radius=1.1927029201011403 gravity=118 pressure=1600 tempK=68 oxygen=false locked=false rings=false rotation=20534 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1420862_99037_1766933 -1420862_99037_1766933 type=ice mass=0.06081940610147002 radius=0.5046332779050597 gravity=24 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=37662 metallicity=1.292165131499846 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1488035_-3034313_-3156807 -1488035_-3034313_-3156807 type=ice mass=0.9712229971716803 radius=1.0104407160721844 gravity=95 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=9188 metallicity=1.0271627977509437 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2011949_3789060_5390006 -2011949_3789060_5390006 type=superearth mass=7.13242712054119 radius=1.8294486117039206 gravity=213 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=24601 metallicity=0.8716874569679396 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -268539_1650411_-2625440 -268539_1650411_-2625440 type=barren mass=0.008533012927036692 radius=0.2601853496316905 gravity=13 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=77028 metallicity=0.5151362933473932 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3063801_-759422_5862386 -3063743_-759422_5862350 type=ice mass=0.6171225552081331 radius=0.8339228503278076 gravity=89 pressure=1600 tempK=189 oxygen=false locked=false rings=false rotation=48269 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3063801_-759422_5862386 -3063792_-759422_5862390 type=desert mass=0.0214744582127459 radius=0.38115810884355894 gravity=15 pressure=0 tempK=246 oxygen=false locked=false rings=false rotation=45982 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3063801_-759422_5862386 -3063799_-759422_5862382 type=lava mass=7.208152273784709 radius=1.6780172812581946 gravity=256 pressure=1600 tempK=915 oxygen=false locked=true rings=false rotation=41175 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3063801_-759422_5862386 -3063801_-759422_5862386 type=lava mass=0.025606191045051286 radius=0.36006011176857755 gravity=20 pressure=0 tempK=1909 oxygen=false locked=true rings=false rotation=76354 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3063801_-759422_5862386 -3063803_-759422_5862385 type=barren mass=0.08511374116631085 radius=0.5061482545056208 gravity=33 pressure=0 tempK=572 oxygen=false locked=true rings=false rotation=35206 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3063801_-759422_5862386 -3063812_-759422_5862406 type=barren mass=0.011811053688285887 radius=0.3235908932389939 gravity=11 pressure=0 tempK=173 oxygen=false locked=false rings=false rotation=7776 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3063801_-759422_5862386 -3063836_-759422_5862361 type=superearth mass=6.341038709712936 radius=1.7102798695984978 gravity=217 pressure=1600 tempK=270 oxygen=false locked=false rings=false rotation=28895 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3063801_-759422_5862386 -3063922_-759422_5862448 type=ice mass=26.458866731696542 radius=2.4658749028334705 gravity=400 pressure=1600 tempK=139 oxygen=false locked=false rings=false rotation=15195 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3359999_5029417_-1201971 -3359999_5029417_-1201971 type=ice mass=0.007999771937505304 radius=0.2756640180131294 gravity=11 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=55540 metallicity=1.512849261669333 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -464282_-3293100_220531 -464282_-3293100_220531 type=ice mass=5.685474320006944 radius=1.7196633145190408 gravity=192 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=76473 metallicity=0.3614643069971393 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -589874_5099752_2648961 -589845_5099756_2648834 type=ice mass=0.5680391705600453 radius=0.8307933662447684 gravity=82 pressure=1600 tempK=68 oxygen=false locked=false rings=false rotation=9715 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -589874_5099752_2648961 -589846_5099755_2649038 type=ice mass=3.326686517747295 radius=1.3085576757295254 gravity=194 pressure=1600 tempK=86 oxygen=false locked=false rings=false rotation=34331 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -589874_5099752_2648961 -589856_5099752_2648936 type=gasgiant mass=152.08354001896822 radius=7.982010427523395 gravity=239 pressure=1600 tempK=148 oxygen=false locked=false rings=false rotation=5558 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -589874_5099752_2648961 -589872_5099751_2648944 type=exotic mass=4.823300939179849 radius=1.5984511753710242 gravity=189 pressure=1600 tempK=217 oxygen=false locked=false rings=false rotation=35891 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -589874_5099752_2648961 -589874_5099752_2648960 type=desert mass=0.04458684623554106 radius=0.4372934321766312 gravity=23 pressure=0 tempK=326 oxygen=false locked=true rings=false rotation=11111 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -589874_5099752_2648961 -589874_5099752_2648961 type=lava mass=0.04313182269041765 radius=0.43322616684121373 gravity=23 pressure=0 tempK=982 oxygen=false locked=true rings=false rotation=11193 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -589874_5099752_2648961 -589875_5099752_2648965 type=desert mass=0.7355676541554571 radius=0.9102675836184433 gravity=89 pressure=100 tempK=216 oxygen=false locked=true rings=false rotation=42274 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -589874_5099752_2648961 -589876_5099752_2648973 type=ice mass=0.12951479986613348 radius=0.5502112106561361 gravity=43 pressure=38 tempK=98 oxygen=false locked=false rings=false rotation=27998 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -589874_5099752_2648961 -590614_5099752_2650343 type=barren mass=0.010025541386882149 radius=0.30302770646596944 gravity=11 pressure=1 tempK=11 oxygen=false locked=false rings=false rotation=35830 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 115515_4884922_-1449848 115515_4884922_-1449848 type=ice mass=17.755262406742364 radius=2.182709991409553 gravity=373 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=11557 metallicity=0.8748781923717313 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1339940_-2645564_6560548 type=ice mass=0.2298729667640338 radius=0.6494371879768814 gravity=55 pressure=342 tempK=48 oxygen=false locked=false rings=false rotation=29397 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340044_-2645561_6560536 type=icegiant mass=39.43566987337176 radius=4.438597591383325 gravity=200 pressure=1600 tempK=161 oxygen=false locked=false rings=false rotation=5168 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340052_-2645562_6560553 type=barren mass=0.009480195427145073 radius=0.27373337680420534 gravity=13 pressure=0 tempK=164 oxygen=false locked=true rings=false rotation=84725 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340053_-2645562_6560556 type=superearth mass=6.231484393967069 radius=1.6681649801111824 gravity=224 pressure=1600 tempK=479 oxygen=false locked=true rings=false rotation=50375 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340056_-2645562_6560558 type=lava mass=0.08537137885037245 radius=0.5029534921198497 gravity=34 pressure=0 tempK=963 oxygen=false locked=true rings=false rotation=11637 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340057_-2645562_6560559 type=barren mass=0.004392948489218785 radius=0.22651086590250133 gravity=9 pressure=0 tempK=362 oxygen=false locked=true rings=false rotation=32902 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340058_-2645562_6560557 type=barren mass=0.0074988963088979815 radius=0.2522099109479354 gravity=12 pressure=0 tempK=288 oxygen=false locked=true rings=false rotation=32275 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340061_-2645562_6560554 type=barren mass=0.015936625855832536 radius=0.3457855663938515 gravity=13 pressure=0 tempK=164 oxygen=false locked=true rings=false rotation=25624 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340064_-2645562_6560567 type=gasgiant mass=102.68767886164126 radius=6.729055091962263 gravity=227 pressure=1600 tempK=237 oxygen=false locked=false rings=true rotation=6932 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340073_-2645562_6560550 type=ice mass=1.336800224292926 radius=1.1216746451827144 gravity=106 pressure=1600 tempK=175 oxygen=false locked=false rings=false rotation=75775 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340102_-2645562_6560536 type=ice mass=0.008509153434487935 radius=0.26426323362501386 gravity=12 pressure=2 tempK=47 oxygen=false locked=false rings=false rotation=17614 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1340056_-2645562_6560558 1340119_-2645561_6560521 type=ice mass=15.11240740295715 radius=2.198756624518602 gravity=313 pressure=1600 tempK=89 oxygen=false locked=false rings=false rotation=25680 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 136345_2380618_4435608 136345_2380618_4435608 type=barren mass=0.10939533716814845 radius=0.5726161013605666 gravity=33 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=12221 metallicity=0.8226319075087267 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2294785_6087239_1852785 2294785_6087239_1852785 type=barren mass=0.02103570917361627 radius=0.3788569313823317 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=72998 metallicity=1.2429109438565753 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2324700_3395074_-773152 2324700_3395074_-773152 type=barren mass=0.013177819722791482 radius=0.32820893486546776 gravity=12 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=25640 metallicity=1.0417666043031906 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2380220_-2985326_2261989 2380220_-2985326_2261989 type=barren mass=0.3739224731395404 radius=0.8176934272940033 gravity=56 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=13304 metallicity=0.8789614972482421 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2455719_-1575919_-1684641 2455719_-1575919_-1684641 type=barren mass=0.004126879491793441 radius=0.23495795814274667 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=13749 metallicity=1.1779993569874176 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4041882_708425_6811475 4041882_708425_6811475 type=ice mass=1.147869400459926 radius=1.029137364803036 gravity=108 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=26087 metallicity=0.7800850208846841 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4374645_5543557_6609018 4374645_5543557_6609018 type=ice mass=1.4116395069521956 radius=1.0349140608384135 gravity=132 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=90951 metallicity=0.3908883587806408 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4760881_3403866_-1378830 4760881_3403866_-1378830 type=ice mass=2.5382063019272625 radius=1.244696857260704 gravity=164 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=44999 metallicity=0.9210678128806111 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5532167_-2797664_-2258523 5532167_-2797664_-2258523 type=barren mass=0.04763152630952036 radius=0.41943619401321175 gravity=27 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=29395 metallicity=1.514197594743003 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5628766_5108790_338559 5628766_5108790_338559 type=ice mass=0.008627549469998762 radius=0.28277463573335193 gravity=11 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=70414 metallicity=1.1612566356055734 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5670369_-3422417_6764355 5670369_-3422417_6764355 type=ice mass=1.2943415034829941 radius=1.0232019832097992 gravity=124 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=6144 metallicity=1.3259025880267747 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5876998_3296879_3027393 5876998_3296879_3027393 type=ice mass=17.45615122214254 radius=2.0861890597953177 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=32628 metallicity=1.2288308932200982 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6068647_-3169217_818787 6068647_-3169217_818787 type=barren mass=0.04958438430747336 radius=0.4506215156332336 gravity=24 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=46987 metallicity=1.1250270844255645 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 668050_2680028_1601017 668050_2680028_1601017 type=ice mass=3.5946845711133975 radius=1.3571907745157803 gravity=195 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=13317 metallicity=1.2317969031138332 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6948671_5049241_-2964475 6948671_5049241_-2964475 type=barren mass=0.010701019301469336 radius=0.3116116058746792 gravity=11 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=14306 metallicity=0.6060910459340265 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 728798_4100023_3876685 728752_4100025_3876660 type=gasgiant mass=219.7087536373542 radius=9.366458180023432 gravity=250 pressure=1600 tempK=117 oxygen=false locked=false rings=true rotation=14162 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 728798_4100023_3876685 728795_4100023_3876676 type=gasgiant mass=114.34606877425351 radius=7.051145770054424 gravity=230 pressure=1600 tempK=272 oxygen=false locked=false rings=true rotation=12235 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 728798_4100023_3876685 728798_4100023_3876682 type=greenhouse mass=8.281666049139647 radius=1.81070986321312 gravity=253 pressure=1600 tempK=441 oxygen=false locked=true rings=false rotation=36126 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 728798_4100023_3876685 728798_4100023_3876685 type=lava mass=8.73584407353523 radius=1.8377454269403521 gravity=259 pressure=237 tempK=1412 oxygen=false locked=true rings=false rotation=34308 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 728798_4100023_3876685 728802_4100023_3876688 type=superearth mass=3.0042019853430992 radius=1.269435908311628 gravity=186 pressure=1596 tempK=403 oxygen=false locked=true rings=false rotation=42325 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 728798_4100023_3876685 728826_4100019_3876763 type=superearth mass=20.75894978211792 radius=2.358873908788583 gravity=373 pressure=1600 tempK=101 oxygen=false locked=false rings=false rotation=43289 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1295452_590737_6017888 id=-871681409 kind=STAR name=PGS--3525313.0.3525313 starTemp=150 starSize=1.656844139099121 + system -1420862_99037_1766933 id=-280597797 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless + system -1488035_-3034313_-3156807 id=-401453429 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless + system -2011949_3789060_5390006 id=-66168209 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system -268539_1650411_-2625440 id=-1871662501 kind=ROGUE_PLANET name=PGR--3525313.0.-3525313 starless + system -3063801_-759422_5862386 id=-1332799893 kind=STAR name=PGS--3525313.-3525313.3525313 starTemp=70 starSize=1.0023287534713745 + system -3359999_5029417_-1201971 id=-63962517 kind=ROGUE_PLANET name=PGR--3525313.3525313.-3525313 starless + system -464282_-3293100_220531 id=-1130433613 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless + system -589874_5099752_2648961 id=-573199273 kind=STAR name=PGS--3525313.3525313.0 starTemp=40 starSize=0.812318742275238 + system 115515_4884922_-1449848 id=-1386063681 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless + system 1340056_-2645562_6560558 id=-1921583641 kind=STAR name=PGS-0.-3525313.3525313 starTemp=40 starSize=0.7821594476699829 + system 136345_2380618_4435608 id=-655881041 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless + system 2294785_6087239_1852785 id=-747899749 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless + system 2324700_3395074_-773152 id=-333451093 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless + system 2380220_-2985326_2261989 id=-846895665 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless + system 2455719_-1575919_-1684641 id=-1093289653 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless + system 4041882_708425_6811475 id=-1527098829 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless + system 4374645_5543557_6609018 id=-138926061 kind=ROGUE_PLANET name=PGR-3525313.3525313.3525313 starless + system 4760881_3403866_-1378830 id=-1839995337 kind=ROGUE_PLANET name=PGR-3525313.0.-3525313 starless + system 5532167_-2797664_-2258523 id=-850423465 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless + system 5628766_5108790_338559 id=-1665662161 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless + system 5670369_-3422417_6764355 id=-862267757 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless + system 5876998_3296879_3027393 id=-286545925 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless + system 6068647_-3169217_818787 id=-621289557 kind=ROGUE_PLANET name=PGR-3525313.-3525313.0 starless + system 668050_2680028_1601017 id=-1525225641 kind=ROGUE_PLANET name=PGR-0.0.0 starless + system 6948671_5049241_-2964475 id=-162398185 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless + system 728798_4100023_3876685 id=-1903899713 kind=STAR name=PGS-0.3525313.3525313 starTemp=40 starSize=0.8625843524932861 seed 2147483647 systems=27 - body -1113821_-4579746_-1376000 -1113821_-4579746_-1376000 kind=MOON orbit=0 radius=0.2350425970115344 starId=-237253153 frame=false - body -1113821_-4579746_-1376000 -1113821_-4579746_-1376000 kind=MOON orbit=0 radius=0.391775468106053 starId=-237253153 frame=false - body -1113821_-4579746_-1376000 -1113821_-4579746_-1376000 kind=ROGUE_PLANET orbit=0 radius=1.2451548691334666 starId=-237253153 frame=true - body -1135507_2621614_-4213570 -1135434_2621617_-4213557 kind=MOON orbit=396 radius=0.3930718179120912 starId=-1468730469 frame=false - body -1135507_2621614_-4213570 -1135434_2621617_-4213557 kind=PLANET orbit=396 radius=0.2053908857293373 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1135440_2621614_-4213505 kind=ASTEROID_BELT orbit=497 radius=0.0 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1135484_2621613_-4213564 kind=MOON orbit=126 radius=0.3493600954378102 starId=-1468730469 frame=false - body -1135507_2621614_-4213570 -1135484_2621613_-4213564 kind=PLANET orbit=126 radius=2.3681492037108587 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=GAS_GIANT orbit=895 radius=8.346217680560606 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=MOON orbit=895 radius=0.21990251326387936 starId=-1468730469 frame=false - body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=MOON orbit=895 radius=0.2248619222244948 starId=-1468730469 frame=false - body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=MOON orbit=895 radius=0.23270214640355974 starId=-1468730469 frame=false - body -1135507_2621614_-4213570 -1135493_2621613_-4213737 kind=MOON orbit=895 radius=0.5150317879288896 starId=-1468730469 frame=false - body -1135507_2621614_-4213570 -1135498_2621613_-4213585 kind=MOON orbit=91 radius=0.24832120239863054 starId=-1468730469 frame=false - body -1135507_2621614_-4213570 -1135498_2621613_-4213585 kind=PLANET orbit=91 radius=0.2004764575690401 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1135505_2621614_-4213571 kind=PLANET orbit=13 radius=1.124285627698082 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1135507_2621614_-4213570 kind=STAR orbit=0 radius=0.0 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1135508_2621614_-4213565 kind=MOON orbit=27 radius=0.5793185118301637 starId=-1468730469 frame=false - body -1135507_2621614_-4213570 -1135508_2621614_-4213565 kind=PLANET orbit=27 radius=0.20468922867125786 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1135586_2621624_-4213820 kind=PLANET orbit=1402 radius=2.023111892962628 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1135838_2621597_-4213313 kind=ASTEROID_BELT orbit=2243 radius=0.0 starId=-1468730469 frame=true - body -1135507_2621614_-4213570 -1137206_2621614_-4193101 kind=STAR orbit=109836 radius=107.86106354176998 starId=-1468730470 frame=true - body -3348450_-3083316_687546 -3348450_-3083316_687546 kind=ROGUE_PLANET orbit=0 radius=0.9166965049835585 starId=-1251094673 frame=true - body -4132292_9308965_8742354 -4132292_9308965_8742354 kind=MOON orbit=0 radius=1.2472387850420885 starId=-1248503277 frame=false - body -4132292_9308965_8742354 -4132292_9308965_8742354 kind=ROGUE_PLANET orbit=0 radius=0.26084449443894825 starId=-1248503277 frame=true - body -4327181_-2482088_8170025 -4326270_-2482086_8172322 kind=MOON orbit=13213 radius=0.4108802270644634 starId=-368134761 frame=false - body -4327181_-2482088_8170025 -4326270_-2482086_8172322 kind=MOON orbit=13213 radius=0.7019849220580543 starId=-368134761 frame=false - body -4327181_-2482088_8170025 -4326270_-2482086_8172322 kind=PLANET orbit=13213 radius=1.9807050957565984 starId=-368134761 frame=true - body -4327181_-2482088_8170025 -4326631_-2482130_8171090 kind=PLANET orbit=6413 radius=1.7911161170913508 starId=-368134761 frame=true - body -4327181_-2482088_8170025 -4326779_-2482104_8170062 kind=MOON orbit=2161 radius=0.24291122518706265 starId=-368134761 frame=false - body -4327181_-2482088_8170025 -4326779_-2482104_8170062 kind=PLANET orbit=2161 radius=1.375903122981076 starId=-368134761 frame=true - body -4327181_-2482088_8170025 -4327072_-2482086_8169891 kind=PLANET orbit=924 radius=1.233461165549056 starId=-368134761 frame=true - body -4327181_-2482088_8170025 -4327118_-2482091_8170311 kind=PLANET orbit=1564 radius=0.31572437773244605 starId=-368134761 frame=true - body -4327181_-2482088_8170025 -4327146_-2482088_8170007 kind=STAR orbit=208 radius=143.9173023247719 starId=-368134763 frame=true - body -4327181_-2482088_8170025 -4327181_-2482088_8170025 kind=STAR orbit=0 radius=0.0 starId=-368134761 frame=true - body -4327181_-2482088_8170025 -4327185_-2482088_8170026 kind=STAR orbit=21 radius=133.5994808936119 starId=-368134762 frame=true - body -4327181_-2482088_8170025 -4327775_-2482075_8169571 kind=MOON orbit=3997 radius=0.4317252201804453 starId=-368134761 frame=false - body -4327181_-2482088_8170025 -4327775_-2482075_8169571 kind=PLANET orbit=3997 radius=0.7437095171035699 starId=-368134761 frame=true - body -4327181_-2482088_8170025 -4328444_-2482075_8173771 kind=ASTEROID_BELT orbit=21140 radius=0.0 starId=-368134761 frame=true - body -4460756_6796900_-655127 -4460756_6796900_-655127 kind=ROGUE_PLANET orbit=0 radius=0.9103973231499702 starId=-195625769 frame=true - body -4890828_1083961_9150975 -4890828_1083961_9150975 kind=MOON orbit=0 radius=0.37614864330229103 starId=-972281605 frame=false - body -4890828_1083961_9150975 -4890828_1083961_9150975 kind=ROGUE_PLANET orbit=0 radius=0.4227515483926527 starId=-972281605 frame=true - body -710985_6200792_981113 -710985_6200792_981113 kind=ROGUE_PLANET orbit=0 radius=0.927347427089166 starId=-1780361145 frame=true - body -728895_4809966_2398711 -728895_4809966_2398711 kind=MOON orbit=0 radius=0.4123351641483745 starId=-1402814529 frame=false - body -728895_4809966_2398711 -728895_4809966_2398711 kind=ROGUE_PLANET orbit=0 radius=1.5241224321858193 starId=-1402814529 frame=true - body 1112499_9349070_8700564 1112351_9349078_8700727 kind=PLANET orbit=1178 radius=1.3538830489987466 starId=-1857127685 frame=true - body 1112499_9349070_8700564 1112480_9349069_8700595 kind=MOON orbit=194 radius=0.34138769245275835 starId=-1857127685 frame=false - body 1112499_9349070_8700564 1112480_9349069_8700595 kind=MOON orbit=194 radius=0.6800089954407418 starId=-1857127685 frame=false - body 1112499_9349070_8700564 1112480_9349069_8700595 kind=PLANET orbit=194 radius=0.3505956426518401 starId=-1857127685 frame=true - body 1112499_9349070_8700564 1112480_9349070_8700517 kind=ASTEROID_BELT orbit=270 radius=0.0 starId=-1857127685 frame=true - body 1112499_9349070_8700564 1112488_9349070_8700570 kind=MOON orbit=68 radius=0.41066276981653405 starId=-1857127685 frame=false - body 1112499_9349070_8700564 1112488_9349070_8700570 kind=PLANET orbit=68 radius=1.1542418946073414 starId=-1857127685 frame=true - body 1112499_9349070_8700564 1112498_9349070_8700557 kind=PLANET orbit=38 radius=1.206794306592984 starId=-1857127685 frame=true - body 1112499_9349070_8700564 1112499_9349070_8700564 kind=STAR orbit=0 radius=0.0 starId=-1857127685 frame=true - body 1112499_9349070_8700564 1112516_9349054_8700047 kind=MOON orbit=2767 radius=0.2053795229785073 starId=-1857127685 frame=false - body 1112499_9349070_8700564 1112516_9349054_8700047 kind=MOON orbit=2767 radius=0.6597061290245418 starId=-1857127685 frame=false - body 1112499_9349070_8700564 1112516_9349054_8700047 kind=PLANET orbit=2767 radius=1.493144341488957 starId=-1857127685 frame=true - body 1112499_9349070_8700564 1112587_9349068_8700587 kind=GAS_GIANT orbit=486 radius=10.180912629340613 starId=-1857127685 frame=true - body 1112499_9349070_8700564 1112587_9349068_8700587 kind=MOON orbit=486 radius=0.23393678785827787 starId=-1857127685 frame=false - body 1112499_9349070_8700564 1112700_9349098_8701367 kind=ASTEROID_BELT orbit=4427 radius=0.0 starId=-1857127685 frame=true - body 1254743_817230_9170965 1254743_817230_9170965 kind=MOON orbit=0 radius=1.4906347986270239 starId=-1270778941 frame=false - body 1254743_817230_9170965 1254743_817230_9170965 kind=ROGUE_PLANET orbit=0 radius=0.21061591475977812 starId=-1270778941 frame=true - body 1388875_8027832_-500528 1388875_8027832_-500528 kind=ROGUE_PLANET orbit=0 radius=0.23853312255247255 starId=-911033505 frame=true - body 1925876_-960979_-1241912 1925279_-960989_-1242304 kind=ASTEROID_BELT orbit=3820 radius=0.0 starId=-1490136521 frame=true - body 1925876_-960979_-1241912 1925834_-960976_-1241971 kind=ASTEROID_BELT orbit=389 radius=0.0 starId=-1490136521 frame=true - body 1925876_-960979_-1241912 1925867_-960979_-1241904 kind=PLANET orbit=68 radius=0.6836214962700502 starId=-1490136521 frame=true - body 1925876_-960979_-1241912 1925870_-960981_-1241861 kind=PLANET orbit=275 radius=0.627027231276017 starId=-1490136521 frame=true - body 1925876_-960979_-1241912 1925876_-960979_-1241912 kind=STAR orbit=0 radius=0.0 starId=-1490136521 frame=true - body 1925876_-960979_-1241912 1926001_-960976_-1241951 kind=GAS_GIANT orbit=701 radius=8.135927550986239 starId=-1490136521 frame=true - body 1925876_-960979_-1241912 1926131_-960995_-1242279 kind=PLANET orbit=2388 radius=0.41444654488793814 starId=-1490136521 frame=true - body 2214240_3293070_-1049364 2214168_3293065_-1049284 kind=ASTEROID_BELT orbit=579 radius=0.0 starId=-1235117197 frame=true - body 2214240_3293070_-1049364 2214176_3293073_-1049342 kind=GAS_GIANT orbit=362 radius=5.400656412552245 starId=-1235117197 frame=true - body 2214240_3293070_-1049364 2214217_3293069_-1049393 kind=ASTEROID_BELT orbit=201 radius=0.0 starId=-1235117197 frame=true - body 2214240_3293070_-1049364 2214235_3293070_-1049372 kind=PLANET orbit=50 radius=0.9514717581328163 starId=-1235117197 frame=true - body 2214240_3293070_-1049364 2214238_3293070_-1049363 kind=PLANET orbit=10 radius=0.7217870748080282 starId=-1235117197 frame=true - body 2214240_3293070_-1049364 2214240_3293070_-1049364 kind=STAR orbit=0 radius=0.0 starId=-1235117197 frame=true - body 2214240_3293070_-1049364 2214244_3293070_-1049361 kind=PLANET orbit=28 radius=0.43863590730784535 starId=-1235117197 frame=true - body 2214240_3293070_-1049364 2214257_3293069_-1049347 kind=MOON orbit=128 radius=0.20152057506693566 starId=-1235117197 frame=false - body 2214240_3293070_-1049364 2214257_3293069_-1049347 kind=MOON orbit=128 radius=0.5060635739817203 starId=-1235117197 frame=false - body 2214240_3293070_-1049364 2214257_3293069_-1049347 kind=PLANET orbit=128 radius=0.8978675689008826 starId=-1235117197 frame=true - body 2857322_-315320_8342875 2857322_-315320_8342875 kind=ROGUE_PLANET orbit=0 radius=0.4250824398370271 starId=-369050573 frame=true - body 4028836_6182067_3322511 4028836_6182067_3322511 kind=MOON orbit=0 radius=0.20713736867197932 starId=-679746033 frame=false - body 4028836_6182067_3322511 4028836_6182067_3322511 kind=MOON orbit=0 radius=2.4431803509202763 starId=-679746033 frame=false - body 4028836_6182067_3322511 4028836_6182067_3322511 kind=ROGUE_PLANET orbit=0 radius=2.3281788126158585 starId=-679746033 frame=true - body 4390556_605832_3499725 4390486_605831_3499932 kind=PLANET orbit=1169 radius=1.9237313197528343 starId=-1579160837 frame=true - body 4390556_605832_3499725 4390541_605830_3499781 kind=GAS_GIANT orbit=310 radius=6.657802907195206 starId=-1579160837 frame=true - body 4390556_605832_3499725 4390541_605830_3499781 kind=MOON orbit=310 radius=0.2844906599851585 starId=-1579160837 frame=false - body 4390556_605832_3499725 4390541_605830_3499781 kind=MOON orbit=310 radius=0.3507788328860036 starId=-1579160837 frame=false - body 4390556_605832_3499725 4390541_605830_3499781 kind=MOON orbit=310 radius=0.5348901167995258 starId=-1579160837 frame=false - body 4390556_605832_3499725 4390556_605832_3499712 kind=MOON orbit=71 radius=0.21488918641162602 starId=-1579160837 frame=false - body 4390556_605832_3499725 4390556_605832_3499712 kind=MOON orbit=71 radius=0.3079528670494941 starId=-1579160837 frame=false - body 4390556_605832_3499725 4390556_605832_3499712 kind=PLANET orbit=71 radius=1.4602513571395628 starId=-1579160837 frame=true - body 4390556_605832_3499725 4390556_605832_3499725 kind=STAR orbit=0 radius=0.0 starId=-1579160837 frame=true - body 4390556_605832_3499725 4390573_605847_3500882 kind=PLANET orbit=6186 radius=1.511392524350423 starId=-1579160837 frame=true - body 4390556_605832_3499725 4390588_605833_3499721 kind=ASTEROID_BELT orbit=172 radius=0.0 starId=-1579160837 frame=true - body 4390556_605832_3499725 4392124_605915_3500704 kind=ASTEROID_BELT orbit=9897 radius=0.0 starId=-1579160837 frame=true - body 5275734_2263955_4887846 5275734_2263955_4887846 kind=ROGUE_PLANET orbit=0 radius=0.20440075971027813 starId=-1932462241 frame=true - body 6002622_-1792397_1902427 6002622_-1792397_1902427 kind=MOON orbit=0 radius=0.204261590532661 starId=-744566785 frame=false - body 6002622_-1792397_1902427 6002622_-1792397_1902427 kind=ROGUE_PLANET orbit=0 radius=0.9096911216480896 starId=-744566785 frame=true - body 6004696_9339127_232393 6004672_9339127_232357 kind=PLANET orbit=231 radius=2.357264927873378 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004686_9339127_232392 kind=MOON orbit=53 radius=0.4701363463386743 starId=-1711371857 frame=false - body 6004696_9339127_232393 6004686_9339127_232392 kind=PLANET orbit=53 radius=0.642853365230426 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004693_9339127_232391 kind=PLANET orbit=21 radius=1.089837967281112 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004696_9339123_232478 kind=GAS_GIANT orbit=457 radius=4.125788820472226 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004696_9339127_232393 kind=STAR orbit=0 radius=0.0 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004696_9339127_232394 kind=MOON orbit=7 radius=0.2921120563045062 starId=-1711371857 frame=false - body 6004696_9339127_232393 6004696_9339127_232394 kind=MOON orbit=7 radius=0.5129551242333938 starId=-1711371857 frame=false - body 6004696_9339127_232393 6004696_9339127_232394 kind=PLANET orbit=7 radius=0.9470450377905923 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004696_9339127_232395 kind=MOON orbit=11 radius=0.29330678016409184 starId=-1711371857 frame=false - body 6004696_9339127_232393 6004696_9339127_232395 kind=MOON orbit=11 radius=0.514929324927059 starId=-1711371857 frame=false - body 6004696_9339127_232393 6004696_9339127_232395 kind=PLANET orbit=11 radius=0.9405115620044691 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004698_9339127_232374 kind=MOON orbit=101 radius=0.6776233155918227 starId=-1711371857 frame=false - body 6004696_9339127_232393 6004698_9339127_232374 kind=PLANET orbit=101 radius=0.21443564201255633 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004701_9339127_232388 kind=PLANET orbit=39 radius=0.5701729389426105 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004711_9339127_232402 kind=ASTEROID_BELT orbit=96 radius=0.0 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004723_9339127_232710 kind=STAR orbit=1703 radius=106.38669212222099 starId=-1711371858 frame=true - body 6004696_9339127_232393 6004728_9339126_232398 kind=GAS_GIANT orbit=174 radius=6.557324680850876 starId=-1711371857 frame=true - body 6004696_9339127_232393 6004738_9339122_232263 kind=ASTEROID_BELT orbit=731 radius=0.0 starId=-1711371857 frame=true - body 6582583_-4715080_-2906356 6582583_-4715080_-2906356 kind=ROGUE_PLANET orbit=0 radius=0.35124043428512763 starId=-326789457 frame=true - body 7907701_7890188_-1932723 7907701_7890188_-1932723 kind=ROGUE_PLANET orbit=0 radius=0.3558343786814908 starId=-1524968341 frame=true - body 8875024_4661563_-4247235 8874321_4661577_-4247372 kind=MOON orbit=3831 radius=0.4501159637054517 starId=-1543301561 frame=false - body 8875024_4661563_-4247235 8874321_4661577_-4247372 kind=PLANET orbit=3831 radius=2.2945996126426924 starId=-1543301561 frame=true - body 8875024_4661563_-4247235 8874986_4661563_-4247263 kind=STAR orbit=253 radius=120.14879344582558 starId=-1543301562 frame=true - body 8875024_4661563_-4247235 8875017_4661563_-4247238 kind=PLANET orbit=40 radius=0.7255437882642279 starId=-1543301561 frame=true - body 8875024_4661563_-4247235 8875024_4661563_-4247235 kind=STAR orbit=0 radius=0.0 starId=-1543301561 frame=true - body 8875024_4661563_-4247235 8875042_4661566_-4246951 kind=PLANET orbit=1521 radius=1.8172110521780322 starId=-1543301561 frame=true - body 8875024_4661563_-4247235 8875090_4661521_-4246092 kind=ASTEROID_BELT orbit=6129 radius=0.0 starId=-1543301561 frame=true - body 8917875_8694620_8962618 8917875_8694620_8962618 kind=ROGUE_PLANET orbit=0 radius=1.0421460729938614 starId=-915858833 frame=true - body 902266_-3556779_3602641 902263_-3556779_3602643 kind=MOON orbit=17 radius=0.2114290419344704 starId=-231618813 frame=false - body 902266_-3556779_3602641 902263_-3556779_3602643 kind=MOON orbit=17 radius=0.42991321352492873 starId=-231618813 frame=false - body 902266_-3556779_3602641 902263_-3556779_3602643 kind=PLANET orbit=17 radius=2.135197106736538 starId=-231618813 frame=true - body 902266_-3556779_3602641 902264_-3556779_3602630 kind=MOON orbit=62 radius=0.249259439662258 starId=-231618813 frame=false - body 902266_-3556779_3602641 902264_-3556779_3602630 kind=MOON orbit=62 radius=0.25170238149933355 starId=-231618813 frame=false - body 902266_-3556779_3602641 902264_-3556779_3602630 kind=PLANET orbit=62 radius=0.2367929860706314 starId=-231618813 frame=true - body 902266_-3556779_3602641 902265_-3556779_3602639 kind=PLANET orbit=11 radius=2.1133071269095636 starId=-231618813 frame=true - body 902266_-3556779_3602641 902266_-3556779_3602641 kind=STAR orbit=0 radius=0.0 starId=-231618813 frame=true - body 902266_-3556779_3602641 902275_-3556777_3602589 kind=PLANET orbit=280 radius=1.96376357855455 starId=-231618813 frame=true - body 902266_-3556779_3602641 902284_-3556780_3602637 kind=MOON orbit=99 radius=0.6885499955857806 starId=-231618813 frame=false - body 902266_-3556779_3602641 902284_-3556780_3602637 kind=PLANET orbit=99 radius=0.44015553033561494 starId=-231618813 frame=true - body 902266_-3556779_3602641 902312_-3556778_3602711 kind=ASTEROID_BELT orbit=448 radius=0.0 starId=-231618813 frame=true - body 9414939_-4025819_8541847 9413578_-4025859_8545318 kind=GAS_GIANT orbit=19937 radius=6.972276140400643 starId=-1030351829 frame=true - body 9414939_-4025819_8541847 9414674_-4025830_8541837 kind=ASTEROID_BELT orbit=1421 radius=0.0 starId=-1030351829 frame=true - body 9414939_-4025819_8541847 9414855_-4025835_8541376 kind=GAS_GIANT orbit=2559 radius=5.97251947898175 starId=-1030351829 frame=true - body 9414939_-4025819_8541847 9414934_-4025819_8541970 kind=STAR orbit=660 radius=112.918562053442 starId=-1030351830 frame=true - body 9414939_-4025819_8541847 9414939_-4025819_8541847 kind=STAR orbit=0 radius=0.0 starId=-1030351829 frame=true - body 9414939_-4025819_8541847 9420876_-4025560_8542366 kind=ASTEROID_BELT orbit=31899 radius=0.0 starId=-1030351829 frame=true - body 9684198_4380843_7885519 9684198_4380843_7885519 kind=MOON orbit=0 radius=0.217218126585063 starId=-148558149 frame=false - body 9684198_4380843_7885519 9684198_4380843_7885519 kind=MOON orbit=0 radius=1.154401378900373 starId=-148558149 frame=false - body 9684198_4380843_7885519 9684198_4380843_7885519 kind=ROGUE_PLANET orbit=0 radius=0.6238414723722325 starId=-148558149 frame=true - derived -1113821_-4579746_-1376000 -1113821_-4579746_-1376000 type=ice mass=2.6080738648000046 radius=1.2451548691334666 gravity=168 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=40037 metallicity=0.934755314038844 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135434_2621617_-4213557 type=barren mass=0.003368670586447824 radius=0.2053908857293373 gravity=8 pressure=0 tempK=99 oxygen=false locked=false rings=false rotation=56659 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135440_2621614_-4213505 type=ice mass=1.481066875554912 radius=1.0776831807317033 gravity=128 pressure=1600 tempK=164 oxygen=false locked=false rings=false rotation=87805 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135484_2621613_-4213564 type=greenhouse mass=18.452700288558923 radius=2.3681492037108587 gravity=329 pressure=1600 tempK=290 oxygen=false locked=false rings=false rotation=24347 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135493_2621613_-4213737 type=icegiant mass=168.51954248437815 radius=8.346217680560606 gravity=242 pressure=1600 tempK=129 oxygen=false locked=false rings=true rotation=7468 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135498_2621613_-4213585 type=ice mass=0.002549949635953622 radius=0.2004764575690401 gravity=6 pressure=0 tempK=170 oxygen=false locked=false rings=false rotation=65034 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135505_2621614_-4213571 type=desert mass=1.8555431410975376 radius=1.124285627698082 gravity=147 pressure=92 tempK=572 oxygen=false locked=true rings=false rotation=31157 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135507_2621614_-4213570 type=lava mass=1.074139711744183 radius=1.050775516633949 gravity=97 pressure=1 tempK=1996 oxygen=false locked=true rings=true rotation=84517 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135508_2621614_-4213565 type=barren mass=0.0025500744378959034 radius=0.20468922867125786 gravity=6 pressure=0 tempK=382 oxygen=false locked=true rings=false rotation=27163 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135586_2621624_-4213820 type=ice mass=11.779809090176194 radius=2.023111892962628 gravity=288 pressure=1600 tempK=97 oxygen=false locked=false rings=false rotation=19040 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1135838_2621597_-4213313 type=ice mass=1.2786757557684811 radius=1.110681274660967 gravity=104 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=75020 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1135507_2621614_-4213570 -1137206_2621614_-4193101 type=gasgiant mass=84.75185176231003 radius=6.1902277988256245 gravity=221 pressure=1600 tempK=12 oxygen=false locked=false rings=true rotation=5794 metallicity=1.4398667353288017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3348450_-3083316_687546 -3348450_-3083316_687546 type=ice mass=0.7895980120698235 radius=0.9166965049835585 gravity=94 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=7530 metallicity=1.4522943313876246 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4132292_9308965_8742354 -4132292_9308965_8742354 type=ice mass=0.006787266085944213 radius=0.26084449443894825 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=36025 metallicity=1.3962911538202278 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4326270_-2482086_8172322 type=superearth mass=11.741516846238405 radius=1.9807050957565984 gravity=299 pressure=1600 tempK=105 oxygen=false locked=false rings=false rotation=79673 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4326631_-2482130_8171090 type=ice mass=9.991527950948676 radius=1.7911161170913508 gravity=311 pressure=1600 tempK=131 oxygen=false locked=false rings=false rotation=68697 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4326779_-2482104_8170062 type=exotic mass=2.8472500726464136 radius=1.375903122981076 gravity=150 pressure=1600 tempK=260 oxygen=false locked=false rings=false rotation=10880 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4327072_-2482086_8169891 type=greenhouse mass=2.2878431586379064 radius=1.233461165549056 gravity=150 pressure=1600 tempK=306 oxygen=false locked=false rings=false rotation=50509 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4327118_-2482091_8170311 type=ice mass=0.013195151898327581 radius=0.31572437773244605 gravity=13 pressure=0 tempK=118 oxygen=false locked=false rings=false rotation=7367 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4327146_-2482088_8170007 type=desert mass=0.021984798863282966 radius=0.3494094803570291 gravity=18 pressure=0 tempK=350 oxygen=false locked=false rings=false rotation=49236 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4327181_-2482088_8170025 type=lava mass=23.501518662832556 radius=2.3580345746047584 gravity=400 pressure=67 tempK=4851 oxygen=false locked=true rings=false rotation=24155 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4327185_-2482088_8170026 type=lava mass=15.617620240213085 radius=2.0737052949692467 gravity=363 pressure=245 tempK=1495 oxygen=false locked=true rings=false rotation=10671 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4327775_-2482075_8169571 type=ice mass=0.2847827010675529 radius=0.7437095171035699 gravity=51 pressure=158 tempK=93 oxygen=false locked=false rings=false rotation=37617 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4327181_-2482088_8170025 -4328444_-2482075_8173771 type=ice mass=24.904024376060658 radius=2.298694700106472 gravity=400 pressure=1600 tempK=72 oxygen=false locked=false rings=false rotation=48629 metallicity=0.7240432076687877 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4460756_6796900_-655127 -4460756_6796900_-655127 type=ice mass=0.8515631132605336 radius=0.9103973231499702 gravity=103 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=22874 metallicity=0.6054647811761072 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -4890828_1083961_9150975 -4890828_1083961_9150975 type=barren mass=0.036172393673821295 radius=0.4227515483926527 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=15728 metallicity=0.8447497217111585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -710985_6200792_981113 -710985_6200792_981113 type=ice mass=0.8717414829472112 radius=0.927347427089166 gravity=101 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=11959 metallicity=0.35374578149423735 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -728895_4809966_2398711 -728895_4809966_2398711 type=superearth mass=5.842756906230048 radius=1.5241224321858193 gravity=252 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=42107 metallicity=0.4096887292822291 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1112499_9349070_8700564 1112351_9349078_8700727 type=ice mass=3.1702058952069962 radius=1.3538830489987466 gravity=173 pressure=1600 tempK=146 oxygen=false locked=false rings=false rotation=6364 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1112499_9349070_8700564 1112480_9349069_8700595 type=barren mass=0.01609332936099729 radius=0.3505956426518401 gravity=13 pressure=0 tempK=195 oxygen=false locked=false rings=false rotation=39980 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1112499_9349070_8700564 1112480_9349070_8700517 type=gasgiant mass=263.9517328332345 radius=10.144196205432351 gravity=257 pressure=1600 tempK=323 oxygen=false locked=false rings=false rotation=11912 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1112499_9349070_8700564 1112488_9349070_8700570 type=greenhouse mass=2.047291102547288 radius=1.1542418946073414 gravity=154 pressure=689 tempK=439 oxygen=false locked=false rings=false rotation=26746 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1112499_9349070_8700564 1112498_9349070_8700557 type=greenhouse mass=2.388601888602027 radius=1.206794306592984 gravity=164 pressure=243 tempK=452 oxygen=false locked=true rings=false rotation=68997 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1112499_9349070_8700564 1112499_9349070_8700564 type=lava mass=0.14621969817118385 radius=0.6086364441260886 gravity=39 pressure=0 tempK=2736 oxygen=false locked=true rings=false rotation=14411 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1112499_9349070_8700564 1112516_9349054_8700047 type=ice mass=3.800366081479916 radius=1.493144341488957 gravity=170 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=24054 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1112499_9349070_8700564 1112587_9349068_8700587 type=icegiant mass=266.1542332383445 radius=10.180912629340613 gravity=257 pressure=1600 tempK=241 oxygen=false locked=false rings=false rotation=7491 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1112499_9349070_8700564 1112700_9349098_8701367 type=ice mass=0.0123337512601163 radius=0.29596529314031295 gravity=14 pressure=3 tempK=33 oxygen=false locked=false rings=false rotation=7042 metallicity=0.9871224498149892 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1254743_817230_9170965 1254743_817230_9170965 type=barren mass=0.0037430375605988766 radius=0.21061591475977812 gravity=8 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=7253 metallicity=1.1148507197072983 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1388875_8027832_-500528 1388875_8027832_-500528 type=ice mass=0.006116930500553493 radius=0.23853312255247255 gravity=11 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=16811 metallicity=0.6973509409375414 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1925876_-960979_-1241912 1925279_-960989_-1242304 type=ice mass=0.0030659681983630305 radius=0.21142585317222323 gravity=7 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=43679 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1925876_-960979_-1241912 1925834_-960976_-1241971 type=barren mass=0.10065146235216979 radius=0.5795739018707657 gravity=30 pressure=5 tempK=152 oxygen=false locked=false rings=false rotation=93186 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1925876_-960979_-1241912 1925867_-960979_-1241904 type=desert mass=0.28795346964131435 radius=0.6836214962700502 gravity=62 pressure=2 tempK=344 oxygen=false locked=false rings=false rotation=68436 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1925876_-960979_-1241912 1925870_-960981_-1241861 type=ice mass=0.19230972517521197 radius=0.627027231276017 gravity=49 pressure=10 tempK=148 oxygen=false locked=false rings=false rotation=31018 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1925876_-960979_-1241912 1925876_-960979_-1241912 type=lava mass=0.003807051127491194 radius=0.23331342623334905 gravity=7 pressure=0 tempK=3023 oxygen=false locked=true rings=false rotation=22920 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1925876_-960979_-1241912 1926001_-960976_-1241951 type=icegiant mass=158.91328712368997 radius=8.135927550986239 gravity=240 pressure=1600 tempK=221 oxygen=false locked=false rings=true rotation=6488 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1925876_-960979_-1241912 1926131_-960995_-1242279 type=barren mass=0.029325698836969893 radius=0.41444654488793814 gravity=17 pressure=5 tempK=61 oxygen=false locked=false rings=false rotation=42102 metallicity=0.515010238675137 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2214240_3293070_-1049364 2214168_3293065_-1049284 type=gasgiant mass=167.6182979811282 radius=8.326781453728056 gravity=242 pressure=1600 tempK=80 oxygen=false locked=false rings=true rotation=6485 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2214240_3293070_-1049364 2214176_3293073_-1049342 type=icegiant mass=61.92285986642407 radius=5.400656412552245 gravity=212 pressure=1600 tempK=101 oxygen=false locked=false rings=false rotation=9010 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2214240_3293070_-1049364 2214217_3293069_-1049393 type=icegiant mass=93.7280033148562 radius=6.467186727479397 gravity=224 pressure=1600 tempK=136 oxygen=false locked=false rings=true rotation=5260 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2214240_3293070_-1049364 2214235_3293070_-1049372 type=exotic mass=0.9039005442391228 radius=0.9514717581328163 gravity=100 pressure=602 tempK=233 oxygen=false locked=false rings=false rotation=20452 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2214240_3293070_-1049364 2214238_3293070_-1049363 type=barren mass=0.31856247810292343 radius=0.7217870748080282 gravity=61 pressure=18 tempK=313 oxygen=false locked=true rings=false rotation=51889 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2214240_3293070_-1049364 2214240_3293070_-1049364 type=barren mass=0.11684115568334545 radius=0.5612053102156213 gravity=37 pressure=0 tempK=990 oxygen=false locked=true rings=false rotation=75825 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2214240_3293070_-1049364 2214244_3293070_-1049361 type=ice mass=0.056512753428701996 radius=0.43863590730784535 gravity=29 pressure=2 tempK=153 oxygen=false locked=true rings=false rotation=19706 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2214240_3293070_-1049364 2214257_3293069_-1049347 type=ice mass=0.8035144573944528 radius=0.8978675689008826 gravity=100 pressure=801 tempK=135 oxygen=false locked=false rings=false rotation=94688 metallicity=0.6172460051976365 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2857322_-315320_8342875 2857322_-315320_8342875 type=barren mass=0.04675124401442071 radius=0.4250824398370271 gravity=26 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=21193 metallicity=0.8215279711125094 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4028836_6182067_3322511 4028836_6182067_3322511 type=ice mass=25.185595027865407 radius=2.3281788126158585 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=6334 metallicity=0.9445001714977364 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4390556_605832_3499725 4390486_605831_3499932 type=superearth mass=13.169100524487668 radius=1.9237313197528343 gravity=356 pressure=1600 tempK=269 oxygen=false locked=false rings=false rotation=16737 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4390556_605832_3499725 4390541_605830_3499781 type=gasgiant mass=100.2040081010103 radius=6.657802907195206 gravity=226 pressure=1600 tempK=481 oxygen=false locked=false rings=false rotation=10072 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4390556_605832_3499725 4390556_605832_3499712 type=superearth mass=3.871910372129332 radius=1.4602513571395628 gravity=182 pressure=254 tempK=690 oxygen=false locked=false rings=false rotation=7164 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4390556_605832_3499725 4390556_605832_3499725 type=lava mass=4.134479878177709 radius=1.4847019055371198 gravity=188 pressure=1 tempK=4361 oxygen=false locked=true rings=false rotation=7884 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4390556_605832_3499725 4390573_605847_3500882 type=superearth mass=4.527987623408388 radius=1.511392524350423 gravity=198 pressure=1600 tempK=117 oxygen=false locked=false rings=false rotation=51841 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4390556_605832_3499725 4390588_605833_3499721 type=greenhouse mass=4.965325875969389 radius=1.5841652900266867 gravity=198 pressure=918 tempK=472 oxygen=false locked=false rings=false rotation=7049 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4390556_605832_3499725 4392124_605915_3500704 type=gasgiant mass=136.1770836540945 radius=7.607679548019192 gravity=235 pressure=1600 tempK=85 oxygen=false locked=false rings=true rotation=12251 metallicity=0.8033505263142218 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5275734_2263955_4887846 5275734_2263955_4887846 type=barren mass=0.0029962764594867377 radius=0.20440075971027813 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=8339 metallicity=1.0030073057735065 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6002622_-1792397_1902427 6002622_-1792397_1902427 type=ice mass=0.6573080231667359 radius=0.9096911216480896 gravity=79 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=23266 metallicity=1.5333359065190146 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004672_9339127_232357 type=ice mass=19.383326879044013 radius=2.357264927873378 gravity=349 pressure=1600 tempK=190 oxygen=false locked=false rings=false rotation=32277 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004686_9339127_232392 type=ice mass=0.18729186361623149 radius=0.642853365230426 gravity=45 pressure=46 tempK=126 oxygen=false locked=false rings=false rotation=16661 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004693_9339127_232391 type=exotic mass=1.6739824492680222 radius=1.089837967281112 gravity=141 pressure=503 tempK=374 oxygen=false locked=true rings=false rotation=12167 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004696_9339123_232478 type=gasgiant mass=33.334199633159535 radius=4.125788820472226 gravity=196 pressure=1600 tempK=190 oxygen=false locked=false rings=true rotation=9524 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004696_9339127_232393 type=lava mass=0.6522438446102877 radius=0.9536021273643882 gravity=72 pressure=2 tempK=1075 oxygen=false locked=true rings=false rotation=29084 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004696_9339127_232394 type=desert mass=0.6368905172168323 radius=0.9470450377905923 gravity=71 pressure=22 tempK=382 oxygen=false locked=true rings=false rotation=28448 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004696_9339127_232395 type=desert mass=0.6218442490287343 radius=0.9405115620044691 gravity=70 pressure=37 tempK=305 oxygen=false locked=true rings=false rotation=28658 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004698_9339127_232374 type=ice mass=0.0033572072851826727 radius=0.21443564201255633 gravity=7 pressure=0 tempK=99 oxygen=false locked=false rings=false rotation=22527 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004701_9339127_232388 type=barren mass=0.12453914538684117 radius=0.5701729389426105 gravity=38 pressure=14 tempK=175 oxygen=false locked=true rings=false rotation=25614 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004711_9339127_232402 type=barren mass=0.01054533698309216 radius=0.2765574870401879 gravity=14 pressure=0 tempK=123 oxygen=false locked=false rings=false rotation=6801 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004723_9339127_232710 type=gasgiant mass=298.52266955600913 radius=10.701828526351594 gravity=261 pressure=1600 tempK=160 oxygen=false locked=false rings=true rotation=6731 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004728_9339126_232398 type=icegiant mass=96.75988118720521 radius=6.557324680850876 gravity=225 pressure=1600 tempK=209 oxygen=false locked=false rings=true rotation=9441 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6004696_9339127_232393 6004738_9339122_232263 type=icegiant mass=209.33589845914716 radius=9.171563489691659 gravity=249 pressure=1600 tempK=183 oxygen=false locked=false rings=true rotation=5859 metallicity=1.5992093020298452 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6582583_-4715080_-2906356 6582583_-4715080_-2906356 type=ice mass=0.02221945063321854 radius=0.35124043428512763 gravity=18 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=25977 metallicity=1.047030230273708 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 7907701_7890188_-1932723 7907701_7890188_-1932723 type=ice mass=0.018383606993069193 radius=0.3558343786814908 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=29027 metallicity=0.8482433564262986 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8875024_4661563_-4247235 8874321_4661577_-4247372 type=superearth mass=18.99169895417755 radius=2.2945996126426924 gravity=361 pressure=1600 tempK=107 oxygen=false locked=false rings=false rotation=22548 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8875024_4661563_-4247235 8874986_4661563_-4247263 type=ice mass=0.053901646586943006 radius=0.46577495126394647 gravity=25 pressure=2 tempK=158 oxygen=false locked=false rings=true rotation=25891 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8875024_4661563_-4247235 8875017_4661563_-4247238 type=desert mass=0.25152217164469054 radius=0.7255437882642279 gravity=48 pressure=1 tempK=448 oxygen=false locked=true rings=false rotation=9830 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8875024_4661563_-4247235 8875024_4661563_-4247235 type=lava mass=0.002741328043620735 radius=0.2043323623311955 gravity=7 pressure=0 tempK=3018 oxygen=false locked=true rings=false rotation=77652 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8875024_4661563_-4247235 8875042_4661566_-4246951 type=ice mass=7.317659402671944 radius=1.8172110521780322 gravity=222 pressure=1600 tempK=148 oxygen=false locked=false rings=false rotation=14096 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8875024_4661563_-4247235 8875090_4661521_-4246092 type=gasgiant mass=16.82630813853038 radius=3.0649226527179056 gravity=179 pressure=1600 tempK=78 oxygen=false locked=false rings=true rotation=5787 metallicity=0.8162198762651764 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 8917875_8694620_8962618 8917875_8694620_8962618 type=ice mass=1.2777567430869878 radius=1.0421460729938614 gravity=118 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=48062 metallicity=1.0897872502867014 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 902266_-3556779_3602641 902263_-3556779_3602643 type=superearth mass=15.617220891812584 radius=2.135197106736538 gravity=343 pressure=1600 tempK=440 oxygen=false locked=true rings=false rotation=59572 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 902266_-3556779_3602641 902264_-3556779_3602630 type=ice mass=0.003953003032939178 radius=0.2367929860706314 gravity=7 pressure=0 tempK=89 oxygen=false locked=false rings=false rotation=6058 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 902266_-3556779_3602641 902265_-3556779_3602639 type=greenhouse mass=14.916611343283593 radius=2.1133071269095636 gravity=334 pressure=1600 tempK=422 oxygen=false locked=true rings=false rotation=15591 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 902266_-3556779_3602641 902266_-3556779_3602641 type=barren mass=0.0020358423726517545 radius=0.20047997099920922 gravity=5 pressure=0 tempK=853 oxygen=false locked=true rings=false rotation=14154 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 902266_-3556779_3602641 902275_-3556777_3602589 type=ice mass=14.414213798293174 radius=1.96376357855455 gravity=374 pressure=1600 tempK=94 oxygen=false locked=false rings=false rotation=8345 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 902266_-3556779_3602641 902284_-3556780_3602637 type=barren mass=0.05050336487176186 radius=0.44015553033561494 gravity=26 pressure=5 tempK=85 oxygen=false locked=false rings=false rotation=13592 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 902266_-3556779_3602641 902312_-3556778_3602711 type=icegiant mass=45.7142664842576 radius=4.7330656319620905 gravity=204 pressure=1600 tempK=78 oxygen=false locked=false rings=false rotation=6513 metallicity=0.6968786166095958 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9414939_-4025819_8541847 9413578_-4025859_8545318 type=gasgiant mass=111.42572700047312 radius=6.972276140400643 gravity=229 pressure=1600 tempK=111 oxygen=false locked=false rings=false rotation=14284 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9414939_-4025819_8541847 9414674_-4025830_8541837 type=ice mass=0.2904384889155367 radius=0.7405269019493477 gravity=53 pressure=9 tempK=176 oxygen=false locked=false rings=false rotation=77597 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9414939_-4025819_8541847 9414855_-4025835_8541376 type=gasgiant mass=78.052418822225 radius=5.97251947898175 gravity=219 pressure=1600 tempK=312 oxygen=false locked=false rings=true rotation=13836 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9414939_-4025819_8541847 9414934_-4025819_8541970 type=superearth mass=21.19914300628885 radius=2.445134938265847 gravity=355 pressure=1600 tempK=668 oxygen=false locked=false rings=false rotation=42946 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9414939_-4025819_8541847 9414939_-4025819_8541847 type=unclassified mass=0.08766661784211546 radius=0.5507068140293626 gravity=29 pressure=0 tempK=7629 oxygen=false locked=true rings=false rotation=15814 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9414939_-4025819_8541847 9420876_-4025560_8542366 type=barren mass=0.008210776742651797 radius=0.2679348082914701 gravity=11 pressure=1 tempK=45 oxygen=false locked=false rings=false rotation=91180 metallicity=0.8044663713931901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 9684198_4380843_7885519 9684198_4380843_7885519 type=barren mass=0.13794446152220974 radius=0.6238414723722325 gravity=35 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=11524 metallicity=0.8696367147827484 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1113821_-4579746_-1376000 id=-237253153 kind=ROGUE_PLANET name=PGR--5002361.-5002361.-5002361 starless - system -1135507_2621614_-4213570 id=-1468730469 kind=STAR name=PGS--5002361.0.-5002361 starTemp=70 starSize=1.0959510803222656 - system -3348450_-3083316_687546 id=-1251094673 kind=ROGUE_PLANET name=PGR--5002361.-5002361.0 starless - system -4132292_9308965_8742354 id=-1248503277 kind=ROGUE_PLANET name=PGR--5002361.5002361.5002361 starless - system -4327181_-2482088_8170025 id=-368134761 kind=STAR name=PGS--5002361.-5002361.5002361 starTemp=150 starSize=1.3601762056350708 - system -4460756_6796900_-655127 id=-195625769 kind=ROGUE_PLANET name=PGR--5002361.5002361.-5002361 starless - system -4890828_1083961_9150975 id=-972281605 kind=ROGUE_PLANET name=PGR--5002361.0.5002361 starless - system -710985_6200792_981113 id=-1780361145 kind=ROGUE_PLANET name=PGR--5002361.5002361.0 starless - system -728895_4809966_2398711 id=-1402814529 kind=ROGUE_PLANET name=PGR--5002361.0.0 starless - system 1112499_9349070_8700564 id=-1857127685 kind=STAR name=PGS-0.5002361.5002361 starTemp=100 starSize=1.009089469909668 - system 1254743_817230_9170965 id=-1270778941 kind=ROGUE_PLANET name=PGR-0.0.5002361 starless - system 1388875_8027832_-500528 id=-911033505 kind=ROGUE_PLANET name=PGR-0.5002361.-5002361 starless - system 1925876_-960979_-1241912 id=-1490136521 kind=STAR name=PGS-0.-5002361.-5002361 starTemp=100 starSize=1.2317028045654297 - system 2214240_3293070_-1049364 id=-1235117197 kind=STAR name=PGS-0.0.-5002361 starTemp=40 starSize=0.834796667098999 - system 2857322_-315320_8342875 id=-369050573 kind=ROGUE_PLANET name=PGR-0.-5002361.5002361 starless - system 4028836_6182067_3322511 id=-679746033 kind=ROGUE_PLANET name=PGR-0.5002361.0 starless - system 4390556_605832_3499725 id=-1579160837 kind=STAR name=PGS-0.0.0 starTemp=150 starSize=1.138994812965393 - system 5275734_2263955_4887846 id=-1932462241 kind=ROGUE_PLANET name=PGR-5002361.0.0 starless - system 6002622_-1792397_1902427 id=-744566785 kind=ROGUE_PLANET name=PGR-5002361.-5002361.0 starless - system 6004696_9339127_232393 id=-1711371857 kind=STAR name=PGS-5002361.5002361.0 starTemp=40 starSize=0.974504828453064 - system 6582583_-4715080_-2906356 id=-326789457 kind=ROGUE_PLANET name=PGR-5002361.-5002361.-5002361 starless - system 7907701_7890188_-1932723 id=-1524968341 kind=ROGUE_PLANET name=PGR-5002361.5002361.-5002361 starless - system 8875024_4661563_-4247235 id=-1543301561 kind=STAR name=PGS-5002361.0.-5002361 starTemp=100 starSize=1.227363109588623 - system 8917875_8694620_8962618 id=-915858833 kind=ROGUE_PLANET name=PGR-5002361.5002361.5002361 starless - system 902266_-3556779_3602641 id=-231618813 kind=STAR name=PGS-0.-5002361.0 starTemp=40 starSize=0.6209897398948669 - system 9414939_-4025819_8541847 id=-1030351829 kind=STAR name=PGS-5002361.-5002361.5002361 starTemp=220 starSize=1.8374673128128052 - system 9684198_4380843_7885519 id=-148558149 kind=ROGUE_PLANET name=PGR-5002361.0.5002361 starless + body -1396847_-3424625_-1950936 -1396847_-3424625_-1950936 kind=MOON orbit=0 radius=0.380681397036819 starId=-207345417 frame=false at=-14891,0,-53569 + body -1396847_-3424625_-1950936 -1396847_-3424625_-1950936 kind=MOON orbit=0 radius=0.38228172882424566 starId=-207345417 frame=false at=28188,0,-22393 + body -1396847_-3424625_-1950936 -1396847_-3424625_-1950936 kind=ROGUE_PLANET orbit=0 radius=0.20025427508835883 starId=-207345417 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166603_6365360_-3072722 kind=ASTEROID_BELT orbit=659 radius=0.0 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166687_6365359_-3072746 kind=GAS_GIANT orbit=234 radius=6.27869866603346 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=GAS_GIANT orbit=412 radius=8.89748111132246 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=MOON orbit=412 radius=0.47056308784713413 starId=-1917888101 frame=false at=-1133479,0,-2473895 + body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=MOON orbit=412 radius=0.5454668740385029 starId=-1917888101 frame=false at=-603139,0,-774443 + body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=MOON orbit=412 radius=0.6110944197764181 starId=-1917888101 frame=false at=-2576920,0,-420480 + body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=MOON orbit=412 radius=0.7214649640195177 starId=-1917888101 frame=false at=547510,0,-590406 + body -166709_6365358_-3072784 -166690_6365359_-3072797 kind=PLANET orbit=125 radius=1.250836965969712 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166705_6365358_-3072786 kind=ASTEROID_BELT orbit=26 radius=0.0 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166709_6365358_-3072784 kind=STAR orbit=0 radius=0.0 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=GAS_GIANT orbit=48 radius=6.258748728626493 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.26863908701994194 starId=-1917888101 frame=false at=698868,0,1262013 + body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.3051035532919284 starId=-1917888101 frame=false at=425871,0,-656818 + body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.3611089191625616 starId=-1917888101 frame=false at=-599683,0,-1775882 + body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.5302972455859541 starId=-1917888101 frame=false at=-781246,0,-104467 + body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.6445900164109255 starId=-1917888101 frame=false at=-1051155,0,-488690 + body -166709_6365358_-3072784 -166710_6365358_-3072783 kind=MOON orbit=6 radius=0.27729654812010507 starId=-1917888101 frame=false at=99577,0,135060 + body -166709_6365358_-3072784 -166710_6365358_-3072783 kind=MOON orbit=6 radius=0.662969993722506 starId=-1917888101 frame=false at=-127913,0,-40595 + body -166709_6365358_-3072784 -166710_6365358_-3072783 kind=PLANET orbit=6 radius=0.7448218246108711 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166710_6365358_-3072786 kind=MOON orbit=12 radius=0.2516749893912134 starId=-1917888101 frame=false at=103027,0,102599 + body -166709_6365358_-3072784 -166710_6365358_-3072786 kind=MOON orbit=12 radius=0.5934694065660353 starId=-1917888101 frame=false at=-115030,0,-6257 + body -166709_6365358_-3072784 -166710_6365358_-3072786 kind=PLANET orbit=12 radius=0.5989009697838469 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166713_6365358_-3072782 kind=MOON orbit=22 radius=0.5660636212701569 starId=-1917888101 frame=false at=-19391,0,163051 + body -166709_6365358_-3072784 -166713_6365358_-3072782 kind=PLANET orbit=22 radius=1.3550701895269492 starId=-1917888101 frame=true at=0,0,0 + body -166709_6365358_-3072784 -166719_6365357_-3072772 kind=GAS_GIANT orbit=83 radius=5.854149891764633 starId=-1917888101 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798630_5881270_282302 kind=PLANET orbit=453 radius=0.48679028766847965 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798643_5881263_282220 kind=ASTEROID_BELT orbit=724 radius=0.0 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798701_5881269_282360 kind=PLANET orbit=113 radius=0.36674069378182417 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798702_5881269_282329 kind=GAS_GIANT orbit=63 radius=6.549907562151851 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798702_5881269_282329 kind=MOON orbit=63 radius=0.2150038482907262 starId=-1883424565 frame=false at=751272,0,529640 + body -1798706_5881269_282340 -1798702_5881269_282329 kind=MOON orbit=63 radius=0.27632207208413984 starId=-1883424565 frame=false at=432230,0,-80156 + body -1798706_5881269_282340 -1798702_5881269_282329 kind=MOON orbit=63 radius=0.3934982191685314 starId=-1883424565 frame=false at=-125888,0,1192574 + body -1798706_5881269_282340 -1798702_5881269_282329 kind=MOON orbit=63 radius=0.45452060090162344 starId=-1883424565 frame=false at=919740,0,1417331 + body -1798706_5881269_282340 -1798704_5881269_282340 kind=PLANET orbit=10 radius=1.262262105304908 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798704_5881269_282342 kind=PLANET orbit=14 radius=1.2471740000967104 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798705_5881269_282345 kind=GAS_GIANT orbit=27 radius=4.113660120859359 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798705_5881269_282345 kind=MOON orbit=27 radius=0.5031994937307466 starId=-1883424565 frame=false at=801138,0,414465 + body -1798706_5881269_282340 -1798706_5881269_282337 kind=ASTEROID_BELT orbit=15 radius=0.0 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798706_5881269_282340 kind=STAR orbit=0 radius=0.0 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798715_5881267_282297 kind=MOON orbit=234 radius=0.6837647158758842 starId=-1883424565 frame=false at=23543,0,179261 + body -1798706_5881269_282340 -1798715_5881267_282297 kind=PLANET orbit=234 radius=0.6754460606140396 starId=-1883424565 frame=true at=0,0,0 + body -1798706_5881269_282340 -1798889_5881269_281652 kind=STAR orbit=3808 radius=77.03123949766159 starId=-1883424566 frame=true at=0,0,0 + body -1842127_-2662654_258460 -1842127_-2662654_258460 kind=MOON orbit=0 radius=0.3259772767413664 starId=-1529010057 frame=false at=-123991,0,-104261 + body -1842127_-2662654_258460 -1842127_-2662654_258460 kind=ROGUE_PLANET orbit=0 radius=2.2979679988747774 starId=-1529010057 frame=true at=0,0,0 + body -2750569_1404918_6927999 -2750569_1404918_6927999 kind=ROGUE_PLANET orbit=0 radius=2.265099431122441 starId=-455553521 frame=true at=0,0,0 + body -2794652_-893855_4236484 -2794640_-893853_4236525 kind=PLANET orbit=228 radius=1.0584082441002571 starId=-1572202913 frame=true at=0,0,0 + body -2794652_-893855_4236484 -2794645_-893855_4236486 kind=GAS_GIANT orbit=38 radius=6.950995980399325 starId=-1572202913 frame=true at=0,0,0 + body -2794652_-893855_4236484 -2794645_-893855_4236486 kind=MOON orbit=38 radius=0.2204833365364542 starId=-1572202913 frame=false at=1227764,0,1535753 + body -2794652_-893855_4236484 -2794645_-893855_4236486 kind=MOON orbit=38 radius=0.3358860955076055 starId=-1572202913 frame=false at=-514825,0,428784 + body -2794652_-893855_4236484 -2794645_-893855_4236486 kind=MOON orbit=38 radius=0.52482374499012 starId=-1572202913 frame=false at=422757,0,288117 + body -2794652_-893855_4236484 -2794649_-893858_4236552 kind=ASTEROID_BELT orbit=364 radius=0.0 starId=-1572202913 frame=true at=0,0,0 + body -2794652_-893855_4236484 -2794652_-893855_4236484 kind=STAR orbit=0 radius=0.0 starId=-1572202913 frame=true at=0,0,0 + body -2794652_-893855_4236484 -2794653_-893855_4236482 kind=MOON orbit=9 radius=0.7423671939218839 starId=-1572202913 frame=false at=-31968,0,-35268 + body -2794652_-893855_4236484 -2794653_-893855_4236482 kind=PLANET orbit=9 radius=0.500787448791386 starId=-1572202913 frame=true at=0,0,0 + body -2794652_-893855_4236484 -2794655_-893855_4236482 kind=ASTEROID_BELT orbit=21 radius=0.0 starId=-1572202913 frame=true at=0,0,0 + body -2976937_6586333_3889163 -2976937_6586333_3889163 kind=ROGUE_PLANET orbit=0 radius=1.0532820872104762 starId=-1838315877 frame=true at=0,0,0 + body -3311706_3302146_-2970565 -3311663_3302148_-2970593 kind=MOON orbit=276 radius=0.554297739536862 starId=-36122729 frame=false at=297632,0,-304781 + body -3311706_3302146_-2970565 -3311663_3302148_-2970593 kind=MOON orbit=276 radius=0.6619246899853962 starId=-36122729 frame=false at=-243712,0,-394372 + body -3311706_3302146_-2970565 -3311663_3302148_-2970593 kind=PLANET orbit=276 radius=2.4156495201898482 starId=-36122729 frame=true at=0,0,0 + body -3311706_3302146_-2970565 -3311670_3302148_-2970491 kind=ASTEROID_BELT orbit=441 radius=0.0 starId=-36122729 frame=true at=0,0,0 + body -3311706_3302146_-2970565 -3311699_3302146_-2970539 kind=MOON orbit=142 radius=0.26316241137208385 starId=-36122729 frame=false at=62657,0,-100462 + body -3311706_3302146_-2970565 -3311699_3302146_-2970539 kind=PLANET orbit=142 radius=0.7233642449067668 starId=-36122729 frame=true at=0,0,0 + body -3311706_3302146_-2970565 -3311706_3302146_-2970565 kind=STAR orbit=0 radius=0.0 starId=-36122729 frame=true at=0,0,0 + body -3311706_3302146_-2970565 -3311707_3302146_-2970562 kind=MOON orbit=17 radius=0.42420342171926473 starId=-36122729 frame=false at=164654,0,453005 + body -3311706_3302146_-2970565 -3311707_3302146_-2970562 kind=PLANET orbit=17 radius=1.9825878133469808 starId=-36122729 frame=true at=0,0,0 + body -3311706_3302146_-2970565 -3311707_3302146_-2970563 kind=MOON orbit=12 radius=0.4190719027416059 starId=-36122729 frame=false at=141189,0,459184 + body -3311706_3302146_-2970565 -3311707_3302146_-2970563 kind=PLANET orbit=12 radius=1.9919617781749086 starId=-36122729 frame=true at=0,0,0 + body -3311706_3302146_-2970565 -3311714_3302146_-2970567 kind=MOON orbit=45 radius=0.7298812093705165 starId=-36122729 frame=false at=-75971,0,2090 + body -3311706_3302146_-2970565 -3311714_3302146_-2970567 kind=PLANET orbit=45 radius=0.6237393525805972 starId=-36122729 frame=true at=0,0,0 + body -3311706_3302146_-2970565 -3330110_3302146_-2976634 kind=STAR orbit=103632 radius=82.97344805538654 starId=-36122730 frame=true at=0,0,0 + body -671411_3391485_519485 -671411_3391485_519485 kind=MOON orbit=0 radius=1.645018902035351 starId=-1864177861 frame=false at=49614,0,125983 + body -671411_3391485_519485 -671411_3391485_519485 kind=ROGUE_PLANET orbit=0 radius=0.6047082686992997 starId=-1864177861 frame=true at=0,0,0 + body 1594627_-205296_2117674 1594572_-205298_2117593 kind=ASTEROID_BELT orbit=523 radius=0.0 starId=-1076464245 frame=true at=0,0,0 + body 1594627_-205296_2117674 1594577_-205297_2117639 kind=PLANET orbit=327 radius=1.4294073187663368 starId=-1076464245 frame=true at=0,0,0 + body 1594627_-205296_2117674 1594618_-205296_2117664 kind=PLANET orbit=69 radius=1.6318762672113716 starId=-1076464245 frame=true at=0,0,0 + body 1594627_-205296_2117674 1594625_-205296_2117675 kind=MOON orbit=11 radius=0.21746637483760253 starId=-1076464245 frame=false at=119796,0,-297809 + body 1594627_-205296_2117674 1594625_-205296_2117675 kind=PLANET orbit=11 radius=2.1060434538482697 starId=-1076464245 frame=true at=0,0,0 + body 1594627_-205296_2117674 1594627_-205296_2117674 kind=STAR orbit=0 radius=0.0 starId=-1076464245 frame=true at=0,0,0 + body 1763248_3924110_-951397 1763248_3924110_-951397 kind=MOON orbit=0 radius=0.8915460990645903 starId=-965124545 frame=false at=134187,0,47060 + body 1763248_3924110_-951397 1763248_3924110_-951397 kind=MOON orbit=0 radius=2.298004836008735 starId=-965124545 frame=false at=45410,0,-35760 + body 1763248_3924110_-951397 1763248_3924110_-951397 kind=ROGUE_PLANET orbit=0 radius=0.4785825282492802 starId=-965124545 frame=true at=0,0,0 + body 1859067_623717_-2088070 1859067_623717_-2088070 kind=ROGUE_PLANET orbit=0 radius=0.3155669885034604 starId=-1701373473 frame=true at=0,0,0 + body 1889446_813181_4205893 1889374_813177_4205834 kind=MOON orbit=500 radius=0.41096038094798665 starId=-1019483873 frame=false at=-283871,0,265381 + body 1889446_813181_4205893 1889374_813177_4205834 kind=PLANET orbit=500 radius=1.3029574931420822 starId=-1019483873 frame=true at=0,0,0 + body 1889446_813181_4205893 1889421_813182_4205913 kind=GAS_GIANT orbit=170 radius=5.529884538397504 starId=-1019483873 frame=true at=0,0,0 + body 1889446_813181_4205893 1889421_813182_4205913 kind=MOON orbit=170 radius=0.24874093551107213 starId=-1019483873 frame=false at=-938495,0,245210 + body 1889446_813181_4205893 1889421_813182_4205913 kind=MOON orbit=170 radius=0.32834977436085333 starId=-1019483873 frame=false at=-258248,0,-263289 + body 1889446_813181_4205893 1889432_813181_4205891 kind=MOON orbit=78 radius=0.2601505362079251 starId=-1019483873 frame=false at=-10429,0,-12134 + body 1889446_813181_4205893 1889432_813181_4205891 kind=PLANET orbit=78 radius=0.2173409056280414 starId=-1019483873 frame=true at=0,0,0 + body 1889446_813181_4205893 1889438_813181_4205897 kind=GAS_GIANT orbit=46 radius=6.619662381279651 starId=-1019483873 frame=true at=0,0,0 + body 1889446_813181_4205893 1889438_813181_4205897 kind=MOON orbit=46 radius=0.34173610744735333 starId=-1019483873 frame=false at=-1326668,0,-1327810 + body 1889446_813181_4205893 1889438_813181_4205897 kind=MOON orbit=46 radius=0.3484246080267326 starId=-1019483873 frame=false at=-1454045,0,714250 + body 1889446_813181_4205893 1889438_813181_4205897 kind=MOON orbit=46 radius=0.5295264158608558 starId=-1019483873 frame=false at=-581550,0,-553432 + body 1889446_813181_4205893 1889438_813181_4205897 kind=MOON orbit=46 radius=0.659864562789785 starId=-1019483873 frame=false at=898431,0,198045 + body 1889446_813181_4205893 1889441_813181_4205894 kind=ASTEROID_BELT orbit=25 radius=0.0 starId=-1019483873 frame=true at=0,0,0 + body 1889446_813181_4205893 1889442_813181_4205893 kind=MOON orbit=22 radius=0.7169628691821708 starId=-1019483873 frame=false at=9297,0,24495 + body 1889446_813181_4205893 1889442_813181_4205893 kind=PLANET orbit=22 radius=0.3971078292923061 starId=-1019483873 frame=true at=0,0,0 + body 1889446_813181_4205893 1889445_813181_4205892 kind=MOON orbit=7 radius=0.2549195784817597 starId=-1019483873 frame=false at=-228599,0,69738 + body 1889446_813181_4205893 1889445_813181_4205892 kind=PLANET orbit=7 radius=1.458171431777551 starId=-1019483873 frame=true at=0,0,0 + body 1889446_813181_4205893 1889446_813181_4205893 kind=STAR orbit=0 radius=0.0 starId=-1019483873 frame=true at=0,0,0 + body 1889446_813181_4205893 1889549_813175_4206001 kind=ASTEROID_BELT orbit=800 radius=0.0 starId=-1019483873 frame=true at=0,0,0 + body 2198808_-575566_6365497 2198808_-575566_6365497 kind=ROGUE_PLANET orbit=0 radius=1.905262701085582 starId=-1785782589 frame=true at=0,0,0 + body 2233013_199342_2539057 2232959_199344_2538990 kind=ASTEROID_BELT orbit=459 radius=0.0 starId=-1579160837 frame=true at=0,0,0 + body 2233013_199342_2539057 2233004_199342_2539063 kind=MOON orbit=57 radius=0.28074740541664756 starId=-1579160837 frame=false at=-235858,0,-208797 + body 2233013_199342_2539057 2233004_199342_2539063 kind=PLANET orbit=57 radius=1.1851610699153496 starId=-1579160837 frame=true at=0,0,0 + body 2233013_199342_2539057 2233011_199342_2539058 kind=MOON orbit=9 radius=0.442848895606726 starId=-1579160837 frame=false at=111885,0,-188495 + body 2233013_199342_2539057 2233011_199342_2539058 kind=PLANET orbit=9 radius=2.4364227035505546 starId=-1579160837 frame=true at=0,0,0 + body 2233013_199342_2539057 2233013_199342_2539057 kind=STAR orbit=0 radius=0.0 starId=-1579160837 frame=true at=0,0,0 + body 2233013_199342_2539057 2233058_199344_2539027 kind=MOON orbit=287 radius=0.24108083163357644 starId=-1579160837 frame=false at=-25401,0,-49459 + body 2233013_199342_2539057 2233058_199344_2539027 kind=PLANET orbit=287 radius=0.2667562435907831 starId=-1579160837 frame=true at=0,0,0 + body 2467378_-3394549_-3171449 2467378_-3394549_-3171449 kind=MOON orbit=0 radius=1.0754090655621613 starId=-1420637497 frame=false at=117078,0,175538 + body 2467378_-3394549_-3171449 2467378_-3394549_-3171449 kind=ROGUE_PLANET orbit=0 radius=2.356867373565178 starId=-1420637497 frame=true at=0,0,0 + body 3890455_2004932_-233592 3889392_2004932_-229119 kind=STAR orbit=24586 radius=103.60897379100322 starId=-1175248790 frame=true at=0,0,0 + body 3890455_2004932_-233592 3890445_2004933_-233643 kind=MOON orbit=279 radius=0.5931194317902839 starId=-1175248789 frame=false at=-53650,0,-4009 + body 3890455_2004932_-233592 3890445_2004933_-233643 kind=MOON orbit=279 radius=0.7125636608885282 starId=-1175248789 frame=false at=-43810,0,15883 + body 3890455_2004932_-233592 3890445_2004933_-233643 kind=PLANET orbit=279 radius=0.23580782176504658 starId=-1175248789 frame=true at=0,0,0 + body 3890455_2004932_-233592 3890454_2004932_-233594 kind=MOON orbit=9 radius=0.5131233425406025 starId=-1175248789 frame=false at=366115,0,-71336 + body 3890455_2004932_-233592 3890454_2004932_-233594 kind=MOON orbit=9 radius=0.6584912217257421 starId=-1175248789 frame=false at=258737,0,86460 + body 3890455_2004932_-233592 3890454_2004932_-233594 kind=PLANET orbit=9 radius=1.2566710178770715 starId=-1175248789 frame=true at=0,0,0 + body 3890455_2004932_-233592 3890455_2004932_-233592 kind=STAR orbit=0 radius=0.0 starId=-1175248789 frame=true at=0,0,0 + body 3890455_2004932_-233592 3890463_2004931_-233509 kind=ASTEROID_BELT orbit=446 radius=0.0 starId=-1175248789 frame=true at=0,0,0 + body 3890455_2004932_-233592 3890469_2004932_-233599 kind=PLANET orbit=83 radius=2.2796306052080975 starId=-1175248789 frame=true at=0,0,0 + body 3996531_-3038111_5303430 3996531_-3038111_5303430 kind=ROGUE_PLANET orbit=0 radius=1.9119707660338916 starId=-689584929 frame=true at=0,0,0 + body 4299007_3832131_-2532498 4299007_3832131_-2532498 kind=MOON orbit=0 radius=1.423539876399934 starId=-657806589 frame=false at=192512,0,-334105 + body 4299007_3832131_-2532498 4299007_3832131_-2532498 kind=MOON orbit=0 radius=1.821387511515449 starId=-657806589 frame=false at=-426105,0,-161828 + body 4299007_3832131_-2532498 4299007_3832131_-2532498 kind=ROGUE_PLANET orbit=0 radius=1.8899347763673122 starId=-657806589 frame=true at=0,0,0 + body 5022771_6570089_4140118 5022771_6570089_4140118 kind=ROGUE_PLANET orbit=0 radius=2.2101822535923454 starId=-1402022073 frame=true at=0,0,0 + body 5089392_-2456012_2414774 5089392_-2456012_2414774 kind=ROGUE_PLANET orbit=0 radius=0.7837832198512151 starId=-474729317 frame=true at=0,0,0 + body 5718025_-888233_-3341159 5718020_-888233_-3341168 kind=STAR orbit=57 radius=76.89058984816074 starId=-1451948175 frame=true at=0,0,0 + body 5718025_-888233_-3341159 5718023_-888233_-3341159 kind=GAS_GIANT orbit=13 radius=7.717711568622234 starId=-1451948173 frame=true at=0,0,0 + body 5718025_-888233_-3341159 5718024_-888233_-3341159 kind=ASTEROID_BELT orbit=7 radius=0.0 starId=-1451948173 frame=true at=0,0,0 + body 5718025_-888233_-3341159 5718025_-888233_-3341159 kind=STAR orbit=0 radius=0.0 starId=-1451948173 frame=true at=0,0,0 + body 5718025_-888233_-3341159 5718075_-888232_-3341159 kind=MOON orbit=267 radius=0.6443974314996002 starId=-1451948173 frame=false at=60835,0,-19877 + body 5718025_-888233_-3341159 5718075_-888232_-3341159 kind=PLANET orbit=267 radius=0.2814384436049694 starId=-1451948173 frame=true at=0,0,0 + body 5718025_-888233_-3341159 5718083_-888231_-3341214 kind=ASTEROID_BELT orbit=427 radius=0.0 starId=-1451948173 frame=true at=0,0,0 + body 5718025_-888233_-3341159 5718496_-888233_-3340282 kind=STAR orbit=5324 radius=79.54021711528301 starId=-1451948174 frame=true at=0,0,0 + body 5730187_2991355_6930094 5730187_2991355_6930094 kind=ROGUE_PLANET orbit=0 radius=0.3614012444688947 starId=-940407273 frame=true at=0,0,0 + body 6244948_3150169_3269973 6244948_3150169_3269973 kind=ROGUE_PLANET orbit=0 radius=0.9233026667448736 starId=-1552747849 frame=true at=0,0,0 + body 6537086_5282959_646441 6537086_5282959_646441 kind=MOON orbit=0 radius=0.20551020117068142 starId=-715739421 frame=false at=-3212,0,-194173 + body 6537086_5282959_646441 6537086_5282959_646441 kind=ROGUE_PLANET orbit=0 radius=0.9432117495314174 starId=-715739421 frame=true at=0,0,0 + body 713381_4456830_2132934 713373_4456830_2132927 kind=PLANET orbit=55 radius=1.0653947740642289 starId=-373901957 frame=true at=0,0,0 + body 713381_4456830_2132934 713374_4456833_2133007 kind=ASTEROID_BELT orbit=393 radius=0.0 starId=-373901957 frame=true at=0,0,0 + body 713381_4456830_2132934 713381_4456830_2132934 kind=STAR orbit=0 radius=0.0 starId=-373901957 frame=true at=0,0,0 + body 713381_4456830_2132934 713383_4456830_2132933 kind=PLANET orbit=13 radius=0.20757672260460924 starId=-373901957 frame=true at=0,0,0 + body 713381_4456830_2132934 713393_4456829_2132978 kind=PLANET orbit=246 radius=0.4310713080560144 starId=-373901957 frame=true at=0,0,0 + body 713381_4456830_2132934 714948_4456830_2142291 kind=STAR orbit=50736 radius=80.36679978132248 starId=-373901958 frame=true at=0,0,0 + body 816822_6851185_5059304 814131_6851104_5058681 kind=ASTEROID_BELT orbit=14779 radius=0.0 starId=-547337629 frame=true at=0,0,0 + body 816822_6851185_5059304 815136_6851223_5058930 kind=PLANET orbit=9237 radius=1.5437826210985488 starId=-547337629 frame=true at=0,0,0 + body 816822_6851185_5059304 816691_6851184_5059714 kind=GAS_GIANT orbit=2301 radius=5.685540056018066 starId=-547337629 frame=true at=0,0,0 + body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.26660902700499145 starId=-547337629 frame=false at=802878,0,317020 + body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.4030505774414985 starId=-547337629 frame=false at=-83132,0,911818 + body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.4860389608165845 starId=-547337629 frame=false at=908915,0,-412162 + body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.6472653505203445 starId=-547337629 frame=false at=309950,0,-703551 + body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.713368566614681 starId=-547337629 frame=false at=71318,0,-615482 + body 816822_6851185_5059304 816711_6851188_5059282 kind=PLANET orbit=606 radius=1.7606471887988784 starId=-547337629 frame=true at=0,0,0 + body 816822_6851185_5059304 816817_6851195_5059543 kind=ASTEROID_BELT orbit=1278 radius=0.0 starId=-547337629 frame=true at=0,0,0 + body 816822_6851185_5059304 816818_6851185_5059302 kind=STAR orbit=25 radius=105.6524378335476 starId=-547337630 frame=true at=0,0,0 + body 816822_6851185_5059304 816822_6851185_5059304 kind=STAR orbit=0 radius=0.0 starId=-547337629 frame=true at=0,0,0 + body 816822_6851185_5059304 816843_6851183_5059334 kind=PLANET orbit=199 radius=0.4210403808014127 starId=-547337629 frame=true at=0,0,0 + derived -1396847_-3424625_-1950936 -1396847_-3424625_-1950936 type=ice mass=0.0022618512298017263 radius=0.20025427508835883 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=73211 metallicity=0.4737519889283227 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166603_6365360_-3072722 type=ice mass=3.1151188881989325 radius=1.3556723909355317 gravity=169 pressure=1600 tempK=65 oxygen=false locked=false rings=false rotation=12468 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166687_6365359_-3072746 type=icegiant mass=87.56370257055207 radius=6.27869866603346 gravity=222 pressure=1600 tempK=115 oxygen=false locked=false rings=true rotation=7535 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166690_6365359_-3072709 type=gasgiant mass=195.22625166955882 radius=8.89748111132246 gravity=247 pressure=1600 tempK=87 oxygen=false locked=false rings=true rotation=10198 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166690_6365359_-3072797 type=ice mass=2.8144803726125183 radius=1.250836965969712 gravity=180 pressure=1600 tempK=149 oxygen=false locked=false rings=false rotation=81603 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166705_6365358_-3072786 type=greenhouse mass=18.475531310837795 radius=2.0737910484877493 gravity=400 pressure=1600 tempK=292 oxygen=false locked=true rings=false rotation=53087 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166709_6365358_-3072784 type=lava mass=6.255313763446167 radius=1.617691722855842 gravity=239 pressure=300 tempK=1350 oxygen=false locked=true rings=false rotation=6526 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166710_6365358_-3072775 type=gasgiant mass=86.9251064798208 radius=6.258748728626493 gravity=222 pressure=1600 tempK=255 oxygen=false locked=false rings=true rotation=4921 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166710_6365358_-3072783 type=barren mass=0.38171939719307657 radius=0.7448218246108711 gravity=69 pressure=20 tempK=370 oxygen=false locked=true rings=false rotation=6022 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166710_6365358_-3072786 type=barren mass=0.17055809087109444 radius=0.5989009697838469 gravity=48 pressure=11 tempK=261 oxygen=false locked=true rings=false rotation=7031 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166713_6365358_-3072782 type=superearth mass=3.6561026253538085 radius=1.3550701895269492 gravity=199 pressure=1600 tempK=410 oxygen=false locked=true rings=false rotation=19243 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -166709_6365358_-3072784 -166719_6365357_-3072772 type=icegiant mass=74.54023741745952 radius=5.854149891764633 gravity=218 pressure=1600 tempK=194 oxygen=false locked=false rings=true rotation=7786 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798630_5881270_282302 type=ice mass=0.07138251152104098 radius=0.48679028766847965 gravity=30 pressure=82 tempK=43 oxygen=false locked=false rings=false rotation=12463 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798643_5881263_282220 type=superearth mass=3.539246956665589 radius=1.4754035474396239 gravity=163 pressure=1600 tempK=83 oxygen=false locked=false rings=false rotation=17413 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798701_5881269_282360 type=barren mass=0.028018407051957496 radius=0.36674069378182417 gravity=21 pressure=5 tempK=99 oxygen=false locked=false rings=false rotation=10798 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798702_5881269_282329 type=gasgiant mass=96.50833814034614 radius=6.549907562151851 gravity=225 pressure=1600 tempK=259 oxygen=false locked=false rings=false rotation=5512 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798704_5881269_282340 type=greenhouse mass=2.4094719316185538 radius=1.262262105304908 gravity=151 pressure=330 tempK=368 oxygen=false locked=true rings=false rotation=64562 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798704_5881269_282342 type=greenhouse mass=2.3106388878060296 radius=1.2471740000967104 gravity=149 pressure=476 tempK=341 oxygen=false locked=true rings=false rotation=65521 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798705_5881269_282345 type=gasgiant mass=33.109245123445625 radius=4.113660120859359 gravity=196 pressure=1600 tempK=396 oxygen=false locked=false rings=false rotation=7607 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798706_5881269_282337 type=desert mass=0.2752602140278586 radius=0.698650047123628 gravity=56 pressure=4 tempK=256 oxygen=false locked=true rings=false rotation=8809 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798706_5881269_282340 type=barren mass=0.3016170857300653 radius=0.7153890467213351 gravity=59 pressure=0 tempK=1053 oxygen=false locked=true rings=false rotation=8490 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798715_5881267_282297 type=ice mass=0.21095799019536213 radius=0.6754460606140396 gravity=46 pressure=380 tempK=88 oxygen=false locked=false rings=false rotation=7985 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1798706_5881269_282340 -1798889_5881269_281652 type=ice mass=0.0032045993022915427 radius=0.21220936065605298 gravity=7 pressure=0 tempK=14 oxygen=false locked=false rings=false rotation=38739 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -1842127_-2662654_258460 -1842127_-2662654_258460 type=superearth mass=18.908576007473027 radius=2.2979679988747774 gravity=358 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=52746 metallicity=0.4083297359078128 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2750569_1404918_6927999 -2750569_1404918_6927999 type=superearth mass=16.537905395105522 radius=2.265099431122441 gravity=322 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=38348 metallicity=1.5119926936005776 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2794652_-893855_4236484 -2794640_-893853_4236525 type=ice mass=1.300859990789865 radius=1.0584082441002571 gravity=116 pressure=1600 tempK=114 oxygen=false locked=false rings=false rotation=11737 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2794652_-893855_4236484 -2794645_-893855_4236486 type=gasgiant mass=110.64508584193437 radius=6.950995980399325 gravity=229 pressure=1600 tempK=296 oxygen=false locked=false rings=false rotation=5885 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2794652_-893855_4236484 -2794649_-893858_4236552 type=gasgiant mass=194.87833912654403 radius=8.890583638719136 gravity=247 pressure=1600 tempK=95 oxygen=false locked=false rings=true rotation=7526 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2794652_-893855_4236484 -2794652_-893855_4236484 type=barren mass=0.21816990027666386 radius=0.7049496928906398 gravity=44 pressure=1 tempK=936 oxygen=false locked=true rings=false rotation=45809 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2794652_-893855_4236484 -2794653_-893855_4236482 type=desert mass=0.06555048019117525 radius=0.500787448791386 gravity=26 pressure=0 tempK=294 oxygen=false locked=true rings=false rotation=27759 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2794652_-893855_4236484 -2794655_-893855_4236482 type=superearth mass=12.865860238956149 radius=2.1363005807875832 gravity=282 pressure=1600 tempK=434 oxygen=false locked=true rings=false rotation=13690 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2976937_6586333_3889163 -2976937_6586333_3889163 type=ice mass=1.2383528736883684 radius=1.0532820872104762 gravity=112 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=25933 metallicity=0.8120316374256196 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3311706_3302146_-2970565 -3311663_3302148_-2970593 type=ice mass=20.099637089137612 radius=2.4156495201898482 gravity=344 pressure=1600 tempK=105 oxygen=false locked=false rings=false rotation=12879 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3311706_3302146_-2970565 -3311670_3302148_-2970491 type=icegiant mass=88.02595803239399 radius=6.293088411319346 gravity=222 pressure=1600 tempK=87 oxygen=false locked=false rings=true rotation=13644 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3311706_3302146_-2970565 -3311699_3302146_-2970539 type=ice mass=0.3226737910885433 radius=0.7233642449067668 gravity=62 pressure=484 tempK=108 oxygen=false locked=false rings=false rotation=17584 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3311706_3302146_-2970565 -3311706_3302146_-2970565 type=lava mass=3.903512805890654 radius=1.5025979873529536 gravity=173 pressure=128 tempK=1136 oxygen=false locked=true rings=false rotation=30728 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3311706_3302146_-2970565 -3311707_3302146_-2970562 type=superearth mass=11.479345977202328 radius=1.9825878133469808 gravity=292 pressure=1600 tempK=486 oxygen=false locked=true rings=false rotation=78730 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3311706_3302146_-2970565 -3311707_3302146_-2970563 type=greenhouse mass=11.73253610000442 radius=1.9919617781749086 gravity=296 pressure=1600 tempK=447 oxygen=false locked=true rings=false rotation=79313 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3311706_3302146_-2970565 -3311714_3302146_-2970567 type=ice mass=0.16483083171053134 radius=0.6237393525805972 gravity=42 pressure=34 tempK=115 oxygen=false locked=true rings=false rotation=28666 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3311706_3302146_-2970565 -3330110_3302146_-2976634 type=ice mass=1.9353099849675102 radius=1.1818452121342142 gravity=139 pressure=1600 tempK=5 oxygen=false locked=false rings=false rotation=15563 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -671411_3391485_519485 -671411_3391485_519485 type=ice mass=0.17890138579382228 radius=0.6047082686992997 gravity=49 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=53681 metallicity=0.6063925496141216 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1594627_-205296_2117674 1594572_-205298_2117593 type=gasgiant mass=74.45527086611237 radius=5.851247652687276 gravity=217 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=12378 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1594627_-205296_2117674 1594577_-205297_2117639 type=ice mass=3.411046165706922 radius=1.4294073187663368 gravity=167 pressure=1600 tempK=90 oxygen=false locked=false rings=false rotation=12436 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1594627_-205296_2117674 1594618_-205296_2117664 type=superearth mass=5.867304871492831 radius=1.6318762672113716 gravity=220 pressure=1600 tempK=227 oxygen=false locked=false rings=false rotation=26217 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1594627_-205296_2117674 1594625_-205296_2117675 type=superearth mass=14.367808430048353 radius=2.1060434538482697 gravity=324 pressure=1600 tempK=569 oxygen=false locked=true rings=false rotation=80508 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1594627_-205296_2117674 1594627_-205296_2117674 type=barren mass=0.009908214049765357 radius=0.2821097998980815 gravity=12 pressure=0 tempK=889 oxygen=false locked=true rings=false rotation=49947 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1763248_3924110_-951397 1763248_3924110_-951397 type=barren mass=0.07808937421335045 radius=0.4785825282492802 gravity=34 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=11823 metallicity=0.43939222631776415 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1859067_623717_-2088070 1859067_623717_-2088070 type=barren mass=0.013289739809071662 radius=0.3155669885034604 gravity=13 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=12488 metallicity=1.170886546686213 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1889446_813181_4205893 1889374_813177_4205834 type=ice mass=2.1937532157379875 radius=1.3029574931420822 gravity=129 pressure=1600 tempK=82 oxygen=false locked=false rings=false rotation=27854 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1889446_813181_4205893 1889421_813182_4205913 type=gasgiant mass=65.38391031628528 radius=5.529884538397504 gravity=214 pressure=1600 tempK=150 oxygen=false locked=false rings=false rotation=5200 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1889446_813181_4205893 1889432_813181_4205891 type=ice mass=0.003196048894076365 radius=0.2173409056280414 gravity=7 pressure=0 tempK=93 oxygen=false locked=false rings=false rotation=38068 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1889446_813181_4205893 1889438_813181_4205897 type=gasgiant mass=98.8886335867683 radius=6.619662381279651 gravity=226 pressure=1600 tempK=289 oxygen=false locked=false rings=true rotation=9797 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1889446_813181_4205893 1889441_813181_4205894 type=barren mass=0.021555134264941003 radius=0.3655036429954987 gravity=16 pressure=0 tempK=200 oxygen=false locked=true rings=false rotation=60504 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1889446_813181_4205893 1889442_813181_4205893 type=barren mass=0.03344889295789856 radius=0.3971078292923061 gravity=21 pressure=0 tempK=213 oxygen=false locked=true rings=false rotation=16301 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1889446_813181_4205893 1889445_813181_4205892 type=superearth mass=4.630557107800478 radius=1.458171431777551 gravity=218 pressure=1600 tempK=805 oxygen=false locked=true rings=false rotation=18664 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1889446_813181_4205893 1889446_813181_4205893 type=lava mass=12.173618416473394 radius=1.8776587580181643 gravity=345 pressure=669 tempK=1825 oxygen=false locked=true rings=false rotation=83253 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1889446_813181_4205893 1889549_813175_4206001 type=ice mass=0.25307854023691273 radius=0.7244333784168657 gravity=48 pressure=725 tempK=53 oxygen=false locked=false rings=false rotation=95137 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2198808_-575566_6365497 2198808_-575566_6365497 type=ice mass=10.702316197276062 radius=1.905262701085582 gravity=295 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=30986 metallicity=1.5889836291628834 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2233013_199342_2539057 2232959_199344_2538990 type=icegiant mass=203.06220854297288 radius=9.05102774503219 gravity=248 pressure=1600 tempK=77 oxygen=false locked=false rings=true rotation=7656 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2233013_199342_2539057 2233004_199342_2539063 type=exotic mass=2.037837169566401 radius=1.1851610699153496 gravity=145 pressure=1600 tempK=240 oxygen=false locked=false rings=false rotation=8889 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2233013_199342_2539057 2233011_199342_2539058 type=greenhouse mass=27.079448136395275 radius=2.4364227035505546 gravity=400 pressure=1600 tempK=468 oxygen=false locked=true rings=false rotation=7707 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2233013_199342_2539057 2233013_199342_2539057 type=lava mass=21.45480203020839 radius=2.380824182005102 gravity=379 pressure=869 tempK=1660 oxygen=false locked=true rings=false rotation=12623 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2233013_199342_2539057 2233058_199344_2539027 type=barren mass=0.009069448340871516 radius=0.2667562435907831 gravity=13 pressure=2 tempK=50 oxygen=false locked=false rings=false rotation=27998 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 2467378_-3394549_-3171449 2467378_-3394549_-3171449 type=ice mass=21.53973837804307 radius=2.356867373565178 gravity=388 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=20367 metallicity=1.0632904605963984 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3890455_2004932_-233592 3889392_2004932_-229119 type=ice mass=5.99660384759894 radius=1.5923441601989268 gravity=237 pressure=1600 tempK=26 oxygen=false locked=false rings=false rotation=12470 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3890455_2004932_-233592 3890445_2004933_-233643 type=barren mass=0.005337119337607637 radius=0.23580782176504658 gravity=10 pressure=0 tempK=63 oxygen=false locked=false rings=false rotation=46483 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3890455_2004932_-233592 3890454_2004932_-233594 type=greenhouse mass=2.334929894427855 radius=1.2566710178770715 gravity=148 pressure=654 tempK=462 oxygen=false locked=true rings=false rotation=11396 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3890455_2004932_-233592 3890455_2004932_-233592 type=lava mass=9.183708728610611 radius=1.955186935911926 gravity=240 pressure=375 tempK=1661 oxygen=false locked=true rings=false rotation=16376 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3890455_2004932_-233592 3890463_2004931_-233509 type=gasgiant mass=188.91827342424764 radius=8.771325497937614 gravity=246 pressure=1600 tempK=97 oxygen=false locked=false rings=true rotation=8516 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3890455_2004932_-233592 3890469_2004932_-233599 type=superearth mass=26.12871745362035 radius=2.2796306052080975 gravity=400 pressure=1600 tempK=246 oxygen=false locked=false rings=false rotation=10670 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3996531_-3038111_5303430 3996531_-3038111_5303430 type=superearth mass=9.59325573931079 radius=1.9119707660338916 gravity=262 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=83725 metallicity=0.913772767522187 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4299007_3832131_-2532498 4299007_3832131_-2532498 type=superearth mass=10.196513676437554 radius=1.8899347763673122 gravity=285 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=18997 metallicity=0.35588764546485174 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5022771_6570089_4140118 5022771_6570089_4140118 type=superearth mass=16.822013173824324 radius=2.2101822535923454 gravity=344 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=38517 metallicity=0.6020116330049018 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5089392_-2456012_2414774 5089392_-2456012_2414774 type=ice mass=0.31367799647917355 radius=0.7837832198512151 gravity=51 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=46696 metallicity=1.0065408351864933 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5718025_-888233_-3341159 5718020_-888233_-3341168 type=gasgiant mass=272.7082283176682 radius=10.289164946349796 gravity=258 pressure=1600 tempK=263 oxygen=false locked=false rings=true rotation=7327 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5718025_-888233_-3341159 5718023_-888233_-3341159 type=gasgiant mass=140.7497379219881 radius=7.717711568622234 gravity=236 pressure=1600 tempK=506 oxygen=false locked=false rings=true rotation=13821 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5718025_-888233_-3341159 5718024_-888233_-3341159 type=greenhouse mass=2.619618325045008 radius=1.2580658048627154 gravity=166 pressure=475 tempK=425 oxygen=false locked=true rings=false rotation=29644 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5718025_-888233_-3341159 5718025_-888233_-3341159 type=barren mass=0.2015654551402771 radius=0.6403317551321865 gravity=49 pressure=0 tempK=925 oxygen=false locked=true rings=false rotation=6285 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5718025_-888233_-3341159 5718075_-888232_-3341159 type=ice mass=0.010498482141557965 radius=0.2814384436049694 gravity=13 pressure=1 tempK=54 oxygen=false locked=false rings=false rotation=58420 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5718025_-888233_-3341159 5718083_-888231_-3341214 type=ice mass=0.8650462181584037 radius=0.9273899661488603 gravity=101 pressure=1600 tempK=97 oxygen=false locked=false rings=false rotation=8465 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5718025_-888233_-3341159 5718496_-888233_-3340282 type=icegiant mass=60.34971765958708 radius=5.34056888417804 gravity=212 pressure=1600 tempK=30 oxygen=false locked=false rings=false rotation=7297 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 5730187_2991355_6930094 5730187_2991355_6930094 type=barren mass=0.026437656144149838 radius=0.3614012444688947 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=14698 metallicity=1.094781397207314 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6244948_3150169_3269973 6244948_3150169_3269973 type=ice mass=0.5941041264344065 radius=0.9233026667448736 gravity=70 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=11951 metallicity=0.8666570370881141 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 6537086_5282959_646441 6537086_5282959_646441 type=barren mass=0.7284843849200809 radius=0.9432117495314174 gravity=82 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=17555 metallicity=0.4763162267116338 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 713381_4456830_2132934 713373_4456830_2132927 type=exotic mass=1.3455430172231895 radius=1.0653947740642289 gravity=119 pressure=1600 tempK=266 oxygen=false locked=false rings=false rotation=45031 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 713381_4456830_2132934 713374_4456833_2133007 type=ice mass=9.13046876726154 radius=1.8276728599659229 gravity=273 pressure=1600 tempK=86 oxygen=false locked=false rings=false rotation=15006 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 713381_4456830_2132934 713381_4456830_2132934 type=lava mass=5.398462527781383 radius=1.6840346511279936 gravity=190 pressure=90 tempK=1024 oxygen=false locked=true rings=false rotation=78615 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 713381_4456830_2132934 713383_4456830_2132933 type=barren mass=0.002488821616803771 radius=0.20757672260460924 gravity=6 pressure=0 tempK=257 oxygen=false locked=true rings=false rotation=41839 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 713381_4456830_2132934 713393_4456829_2132978 type=barren mass=0.048572146513674155 radius=0.4310713080560144 gravity=26 pressure=18 tempK=59 oxygen=false locked=false rings=false rotation=32112 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 713381_4456830_2132934 714948_4456830_2142291 type=ice mass=2.7218332020468132 radius=1.3317114481371697 gravity=153 pressure=1600 tempK=8 oxygen=false locked=false rings=false rotation=18149 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 816822_6851185_5059304 814131_6851104_5058681 type=ice mass=0.18633243327335225 radius=0.6273201520127174 gravity=47 pressure=441 tempK=57 oxygen=false locked=false rings=false rotation=6608 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 816822_6851185_5059304 815136_6851223_5058930 type=ice mass=3.8710065602978716 radius=1.5437826210985488 gravity=162 pressure=1600 tempK=100 oxygen=false locked=false rings=false rotation=17376 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 816822_6851185_5059304 816691_6851184_5059714 type=gasgiant mass=69.69456445436751 radius=5.685540056018066 gravity=216 pressure=1600 tempK=212 oxygen=false locked=false rings=false rotation=7071 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 816822_6851185_5059304 816711_6851188_5059282 type=superearth mass=6.626827297311496 radius=1.7606471887988784 gravity=214 pressure=1600 tempK=449 oxygen=false locked=false rings=false rotation=44015 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 816822_6851185_5059304 816817_6851195_5059543 type=superearth mass=17.067554987748622 radius=2.172654173678009 gravity=362 pressure=1600 tempK=309 oxygen=false locked=false rings=false rotation=89074 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 816822_6851185_5059304 816818_6851185_5059302 type=lava mass=2.1018842181699178 radius=1.2823508765013565 gravity=128 pressure=32 tempK=1047 oxygen=false locked=true rings=false rotation=72051 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 816822_6851185_5059304 816822_6851185_5059304 type=lava mass=3.4209093327392703 radius=1.3205047953422844 gravity=196 pressure=1 tempK=5237 oxygen=false locked=true rings=false rotation=45668 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 816822_6851185_5059304 816843_6851183_5059334 type=barren mass=0.04617258486949676 radius=0.4210403808014127 gravity=26 pressure=0 tempK=369 oxygen=false locked=false rings=false rotation=29583 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] + system -1396847_-3424625_-1950936 id=-207345417 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless + system -166709_6365358_-3072784 id=-1917888101 kind=STAR name=PGS--3525313.3525313.-3525313 starTemp=40 starSize=0.7003536820411682 + system -1798706_5881269_282340 id=-1883424565 kind=STAR name=PGS--3525313.3525313.0 starTemp=40 starSize=0.9460086822509766 + system -1842127_-2662654_258460 id=-1529010057 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless + system -2750569_1404918_6927999 id=-455553521 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless + system -2794652_-893855_4236484 id=-1572202913 kind=STAR name=PGS--3525313.-3525313.3525313 starTemp=40 starSize=0.747058093547821 + system -2976937_6586333_3889163 id=-1838315877 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system -3311706_3302146_-2970565 id=-36122729 kind=STAR name=PGS--3525313.0.-3525313 starTemp=40 starSize=0.7600389122962952 + system -671411_3391485_519485 id=-1864177861 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless + system 1594627_-205296_2117674 id=-1076464245 kind=STAR name=PGS-0.-3525313.0 starTemp=40 starSize=0.6730666756629944 + system 1763248_3924110_-951397 id=-965124545 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless + system 1859067_623717_-2088070 id=-1701373473 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless + system 1889446_813181_4205893 id=-1019483873 kind=STAR name=PGS-0.0.3525313 starTemp=40 starSize=0.8574948906898499 + system 2198808_-575566_6365497 id=-1785782589 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless + system 2233013_199342_2539057 id=-1579160837 kind=STAR name=PGS-0.0.0 starTemp=40 starSize=0.6222827434539795 + system 2467378_-3394549_-3171449 id=-1420637497 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless + system 3890455_2004932_-233592 id=-1175248789 kind=STAR name=PGS-3525313.0.-3525313 starTemp=40 starSize=0.94906085729599 + system 3996531_-3038111_5303430 id=-689584929 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless + system 4299007_3832131_-2532498 id=-657806589 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless + system 5022771_6570089_4140118 id=-1402022073 kind=ROGUE_PLANET name=PGR-3525313.3525313.3525313 starless + system 5089392_-2456012_2414774 id=-474729317 kind=ROGUE_PLANET name=PGR-3525313.-3525313.0 starless + system 5718025_-888233_-3341159 id=-1451948173 kind=STAR name=PGS-3525313.-3525313.-3525313 starTemp=40 starSize=0.7285904288291931 + system 5730187_2991355_6930094 id=-940407273 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless + system 6244948_3150169_3269973 id=-1552747849 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless + system 6537086_5282959_646441 id=-715739421 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless + system 713381_4456830_2132934 id=-373901957 kind=STAR name=PGS-0.3525313.0 starTemp=40 starSize=0.7361619472503662 + system 816822_6851185_5059304 id=-547337629 kind=STAR name=PGS-0.3525313.3525313 starTemp=150 starSize=1.6428048610687256 From 31260008cd26a97d1265a3e652ea022a934c0bc4 Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 19 Aug 2026 19:30:20 +0300 Subject: [PATCH 40/42] feat: a telescope looks at a patch of sky, and the field is divided - survey becomes a cone: apex, direction, half-angle, walked outwards - reach derived from the aperture's limiting magnitude, not configured - detection split from characterisation; detection derives no bodies - a look resolves its whole territory instead of one seat of it - star territories divided uniformly, both occupancies per sub-seat - passive radar strides by territory, and reaches a neighbour - seat resolution stops fabricating the system it discards - dust measured only after clear-sky brightness admits the star - golden corpus regenerated against the divided field --- .../advancedRocketry/api/ARConfiguration.java | 40 +- .../command/test/TestProbeCommand.java | 28 +- .../tile/multiblock/TileObservatory.java | 61 +- .../universe/ClusteredGalaxyGenerator.java | 219 +- .../advancedRocketry/universe/ConeWalk.java | 319 +++ .../universe/IGalaxyGenerator.java | 27 + .../advancedRocketry/universe/RegionScan.java | 393 ++-- .../universe/StellarMagnitude.java | 185 ++ .../universe/TelescopeScan.java | 358 ++- .../universe/UniverseRegistry.java | 20 + .../assets/advancedrocketry/lang/en_US.lang | 7 +- .../client/MachineGuiClientGroupE2ETest.java | 5 +- .../test/integration/SystemContentTest.java | 13 +- .../server/TelescopeRegionScanE2ETest.java | 47 +- .../unit/ClusteredGalaxyGeneratorTest.java | 72 +- .../test/unit/NebulaConcealmentTest.java | 7 +- .../test/unit/PlanetRealizationTest.java | 27 +- .../test/unit/StarClusterTest.java | 10 +- .../test/unit/SystemRetinueTest.java | 20 +- .../test/unit/TelescopeConeSurveyTest.java | 498 +++++ .../test/unit/TelescopeRegionScanTest.java | 143 +- .../resources/universe/golden-corpus-v1.txt | 1949 ++--------------- 22 files changed, 2290 insertions(+), 2158 deletions(-) create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/ConeWalk.java create mode 100644 src/main/java/zmaster587/advancedRocketry/universe/StellarMagnitude.java create mode 100644 src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeConeSurveyTest.java diff --git a/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java b/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java index 7dc5a1585..f3408668f 100644 --- a/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java +++ b/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java @@ -286,20 +286,37 @@ public class ARConfiguration { public int terraformPlanetSpeed; @ConfigProperty public int planetDiscoveryChance; + /** + * The shipped telescope-survey defaults, named so that the code that REGISTERS them and the test + * that MEASURES what they cost cannot drift apart. A default whose consequences are stated + * somewhere other than where the default lives is a number nobody is checking. + * + *

    Measured together, at the stock star table and star spacing: a full-depth pointing holds + * about 77 000 looks, reaches 1 768 light years, registers of the order of thirty systems, and + * takes roughly six hundred steps.

    + */ + public static final double DEFAULT_TELESCOPE_LIMITING_MAGNITUDE = 8d; + /** @see #DEFAULT_TELESCOPE_LIMITING_MAGNITUDE */ + public static final double DEFAULT_TELESCOPE_CONE_HALF_ANGLE_DEGREES = 1d; + /** @see #DEFAULT_TELESCOPE_LIMITING_MAGNITUDE */ + public static final int DEFAULT_TELESCOPE_SCAN_MAX_CELLS = 200_000; + /** @see #DEFAULT_TELESCOPE_LIMITING_MAGNITUDE */ + public static final int DEFAULT_TELESCOPE_SCAN_BASE_TICKS = 20; + /** @see #DEFAULT_TELESCOPE_LIMITING_MAGNITUDE */ + public static final int DEFAULT_TELESCOPE_SCAN_CELLS_PER_STEP = 130; + @ConfigProperty - public double telescopeScanRangeLightYears; + public double telescopeLimitingMagnitude; @ConfigProperty - public int telescopeScanHalfWidthSteps; + public double telescopeConeHalfAngleDegrees; @ConfigProperty public int telescopeScanMaxCells; @ConfigProperty public int telescopeScanBaseTicks; @ConfigProperty - public double telescopeScanTicksPerLightYear; - @ConfigProperty public int telescopeScanCellsPerStep; @ConfigProperty - public int telescopePassiveRadiusCells; + public int telescopePassiveRadiusSteps; @ConfigProperty public double telescopeObscuredAtMagnitudes; @ConfigProperty @@ -537,15 +554,14 @@ public static void loadPreInit() { //Planet arConfig.planetsMustBeDiscovered = config.get(PLANET, "planetsMustBeDiscovered", false, "Planets must be discovered in the warp controller before being visible").getBoolean(); arConfig.planetDiscoveryChance = config.get(PLANET, "planetDiscoveryChance", 5, "Chance of planet discovery in the warp controller, chance is 1/n", 1, Integer.MAX_VALUE).getInt(); - arConfig.telescopeScanRangeLightYears = config.get(PLANET, "telescopeScanRangeLightYears", 100d, "How far, in LIGHT YEARS, an observatory's region scan can be aimed. This is the instrument's horizon: beyond it the sky is not resolvable, which is what keeps an endless universe from being read off a telescope. A scan aimed farther is clamped to this. An operator aims in STEPS, and one step is one star's territory (the mean distance to a neighbouring star), so this reach divided by that spacing is how many steps out he may point it.", 0d, Double.MAX_VALUE).getDouble(); - arConfig.telescopeScanHalfWidthSteps = config.get(PLANET, "telescopeScanHalfWidthSteps", 2, "Half-width, in STEPS, of the region one survey sweeps - one step being one star's territory, the same stride the sweep walks by. 0 means a single look, 1 a 3x3x3 patch of neighbouring territories, 2 a 5x5x5, and so on. Narrowed automatically when the resulting region would exceed telescopeScanMaxCells.", 0, Integer.MAX_VALUE).getInt(); - arConfig.telescopeScanMaxCells = config.get(PLANET, "telescopeScanMaxCells", 1000, "Hard ceiling on how many cells one survey may LOOK AT (one per step, not one per cell of sky crossed). The width above is narrowed until the region fits under this. A sweep may be long, but never unbounded.", 1, Integer.MAX_VALUE).getInt(); - arConfig.telescopeScanBaseTicks = config.get(PLANET, "telescopeScanBaseTicks", 200, "Ticks one STEP of a survey takes before distance is counted - the cost of holding the instrument on a patch of sky at all. Only applies with planetsMustBeDiscovered on; without research, an observation is instant.", 0, Integer.MAX_VALUE).getInt(); - arConfig.telescopeScanTicksPerLightYear = config.get(PLANET, "telescopeScanTicksPerLightYear", 20d, "Extra ticks per light year of distance, per step. This is what makes a far region a longer survey than a near one.", 0d, Double.MAX_VALUE).getDouble(); - arConfig.telescopeScanCellsPerStep = config.get(PLANET, "telescopeScanCellsPerStep", 5, "How many cells of the region one step of a survey resolves. This is the bound that stops a sweep from enumerating everything at once.", 1, Integer.MAX_VALUE).getInt(); + arConfig.telescopeLimitingMagnitude = config.get(PLANET, "telescopeLimitingMagnitude", DEFAULT_TELESCOPE_LIMITING_MAGNITUDE, "How faint a star an observatory can still register, in APPARENT MAGNITUDE - the scale astronomy measures brightness on, where SMALLER IS BRIGHTER and five magnitudes is a factor of a hundred in received light. This is the instrument's aperture, and it is what its reach is derived FROM: a survey walks outwards only as far as the brightest star it could possibly see would still be above this limit, so a better aperture reaches farther by seeing more rather than by being told a bigger number. Reference points: 6 is roughly the naked eye, 8 (the default) reaches a sun-like star at about 160 light years and a blue giant at 1360, and each 5 magnitudes multiplies every one of those distances by ten. Dust counts against the same limit, so a cloud in the way shortens the reach in exactly the way distance does.", -30d, 40d).getDouble(); + arConfig.telescopeConeHalfAngleDegrees = config.get(PLANET, "telescopeConeHalfAngleDegrees", DEFAULT_TELESCOPE_CONE_HALF_ANGLE_DEGREES, "How wide a patch of sky one pointing covers, in DEGREES from the axis to the edge. A survey is a cone with its apex at the observatory, so this is its opening: narrow in degrees, and still enormous at the far end because the same angle subtends more space the farther out it is read. Widening it multiplies the work by the SQUARE, so a pointing twice as wide is four times the survey.", 0.001d, 89d).getDouble(); + arConfig.telescopeScanMaxCells = config.get(PLANET, "telescopeScanMaxCells", DEFAULT_TELESCOPE_SCAN_MAX_CELLS, "Hard ceiling on how many LOOKS one survey may hold (one per star territory along the pointing, not one per cell of sky crossed). A pointing that would exceed it is SHORTENED until it fits, exactly as its width used to be narrowed - a sweep may be long, but never unbounded. At the shipped aperture and opening a full-depth pointing holds about 77 000 looks, so this leaves room to raise the aperture a little before the ceiling starts cutting the reach.", 1, Integer.MAX_VALUE).getInt(); + arConfig.telescopeScanBaseTicks = config.get(PLANET, "telescopeScanBaseTicks", DEFAULT_TELESCOPE_SCAN_BASE_TICKS, "Ticks one STEP of a survey takes. A pointing's cost in time is carried by how many steps it needs and not by how far it reaches, because a deeper pointing already holds proportionally more looks. Only applies with planetsMustBeDiscovered on; without research, an observation is instant.", 0, Integer.MAX_VALUE).getInt(); + arConfig.telescopeScanCellsPerStep = config.get(PLANET, "telescopeScanCellsPerStep", DEFAULT_TELESCOPE_SCAN_CELLS_PER_STEP, "How many looks one step of a survey resolves. This is the bound that stops a sweep from enumerating everything at once. With the shipped defaults a full-depth pointing is about 600 steps, i.e. roughly ten minutes of clear night.", 1, Integer.MAX_VALUE).getInt(); arConfig.telescopeSurveyDataPerStep = config.get(PLANET, "telescopeSurveyDataPerStep", 0, "Distance data one step of a survey consumes, drawn from the observatory's data buses the same way its asteroid scan draws. A step with too little data waits rather than resolving, so an unfed instrument stalls instead of working for free. Zero (the default) means a survey costs nothing - what it should cost is a balance question, not a mechanic one.", 0, Integer.MAX_VALUE).getInt(); arConfig.telescopeObscuredAtMagnitudes = config.get(PLANET, "telescopeObscuredAtMagnitudes", 5d, "How much dust a survey can see THROUGH, in magnitudes of visual extinction - the unit astronomy measures interstellar dust in. A nebula between the instrument and what it is looking at dims it; past this much, the survey can still tell that a system is there but can no longer make out its bodies, and writes the bare coordinate instead. The default is the real boundary at which faint objects behind a cloud disappear: ~1 magnitude is noticeable dimming, ~5 is where things start vanishing, ~10 is an opaque dark cloud. Raise it to see through thicker clouds; set it to 0 to turn concealment off entirely.", 0d, Double.MAX_VALUE).getDouble(); - arConfig.telescopePassiveRadiusCells = config.get(PLANET, "telescopePassiveRadiusCells", 2, "How far, in CELLS, the passive local radar reaches around the observatory's own cell. Cells and not star territories: this mode watches the neighbourhood, where the planet in the next cell over is a different destination from its star. Passive costs nothing; the directed survey is what looks far away.", 0, Integer.MAX_VALUE).getInt(); + arConfig.telescopePassiveRadiusSteps = config.get(PLANET, "telescopePassiveRadiusSteps", 1, "How far, in STAR TERRITORIES, the passive local radar reaches around the observatory's own. 0 is the system you are standing in and nothing else; 1 (the default) adds the twenty-six territories around it. Territories and not cells: one look already yields every body of the system that owns it, so a radius counted in cells never reached a neighbour at all - two cells was a fifth of the way to the innermost planet of the system the instrument was already standing in. Passive costs nothing; the pointing is what looks far away.", 0, Integer.MAX_VALUE).getInt(); DimensionManager.dimOffset = config.getInt("minDimension", PLANET, 2, -127, 8000, "Lowest dimension ID that can be used for planets."); arConfig.canPlayerRespawnInSpace = config.get(PLANET, "allowPlanetRespawn", false, "Allow bed respawn on planets with breathable air.").getBoolean(); arConfig.forcePlayerRespawnInSpace = config.get(PLANET, "forcePlanetRespawn", false, "Allow bed respawn on planets even without breathable air. Requires 'allowPlanetRespawn=true'.").getBoolean(); diff --git a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java index 27044ecd7..42a218f96 100644 --- a/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java +++ b/src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java @@ -2642,7 +2642,12 @@ private String telescopeScanFields(zmaster587.advancedRocketry.tile.multiblock.T // What ONE step of that aim is worth in cells — the instrument's own stride, readable // while it is idle, so a fixture can be placed where the next look will actually land. .append(",\"stepCells\":").append(tuning.strideCells()) - .append(",\"passive\":").append(scope.isPassive()); + .append(",\"passive\":").append(scope.isPassive()) + // The aperture and the opening: what the instrument can SEE, which is what its reach + // above is derived from, and how wide a patch one pointing covers. + .append(",\"limitMagnitude\":").append(tuning.limitMagnitude()) + .append(",\"halfAngleDeg\":").append(Math.toDegrees(tuning.halfAngleRadians())) + .append(",\"wholeSystem\":").append(scope.isCharacterisingWholeSystem()); if (scan != null) { // The cell counts ship beside the region they are counted over, and the next deadline // beside the clock it is measured against: a sweep that will not advance must be able to @@ -2662,7 +2667,19 @@ private String telescopeScanFields(zmaster587.advancedRocketry.tile.multiblock.T .append(",\"ticksPerStep\":").append(scan.ticksPerStep()) .append(",\"estimatedTicks\":").append(scan.estimatedTicks()) .append(",\"progress\":").append(scan.progress()) - .append(",\"stepDue\":").append(scan.stepDue(now)); + .append(",\"stepDue\":").append(scan.stepDue(now)) + // Which SHAPE the survey is: a pointing has an apex and an opening, a local radar + // has neither, and a test that cannot tell them apart cannot tell why a sweep + // covered what it covered. + .append(",\"pointing\":").append(scan.isPointing()) + .append(",\"shells\":").append(scan.cone() == null ? 0 : scan.cone().shells()) + // WHERE it is aimed, which the corners no longer say: a cone's bounding box is + // the apex plus its reach on every axis, so re-aiming the same instrument leaves + // min/max untouched. The direction is the aim. + .append(",\"dir\":\"").append(scan.cone() == null ? "" + : String.format(java.util.Locale.ROOT, "%.4f_%.4f_%.4f", + scan.cone().dirX(), scan.cone().dirY(), scan.cone().dirZ())) + .append("\""); } return out.toString(); } @@ -11284,13 +11301,12 @@ private void handleMachineTickUntil(MinecraftServer server, ICommandSender sende // The telescope's reach and what a look costs in time, all read at scan START, // so flipping them at runtime is enough to exercise a short scan in a test // without waiting out a production-length observation. - "telescopeScanRangeLightYears", - "telescopeScanHalfWidthSteps", + "telescopeLimitingMagnitude", + "telescopeConeHalfAngleDegrees", "telescopeScanMaxCells", "telescopeScanBaseTicks", - "telescopeScanTicksPerLightYear", "telescopeScanCellsPerStep", - "telescopePassiveRadiusCells", + "telescopePassiveRadiusSteps", "telescopeSurveyDataPerStep", // How much dust a survey sees through, in magnitudes. Flippable at runtime so a // test can drive BOTH sides of concealment against one generated cloud. diff --git a/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java b/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java index 4108b5691..190661e7a 100644 --- a/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java +++ b/src/main/java/zmaster587/advancedRocketry/tile/multiblock/TileObservatory.java @@ -111,6 +111,7 @@ public class TileObservatory extends TileMultiPowerConsumer implements IModularI private static final byte START_SCAN = 19; private static final byte ABORT_SCAN = 20; private static final byte PASSIVE_SWEEP = 21; + private static final byte TOGGLE_WHOLE_SYSTEM = 22; /** Progress id of the region-scan bar; the machine's own bar keeps id 0. */ private static final int PROGRESS_SCAN = 1; /** @@ -161,6 +162,17 @@ public class TileObservatory extends TileMultiPowerConsumer implements IModularI private int pendingDistanceDelta; /** Watching the neighbourhood rather than a distant patch. The two modes are exclusive. */ private boolean passive; + /** + * Whether a detection is followed all the way to the system's BODIES, or only its address is + * written down. + * + *

    An operational choice with a cost, so it lives on the instrument rather than in the + * configuration: characterising every find fills a crystal many times faster and is what an + * operator wants over known sky, while a deep pointing into sky nobody has been to is a list of + * places worth flying to. Default on, because that is what the survey did before it could tell + * the two questions apart.

    + */ + private boolean characteriseWholeSystem = true; public TileObservatory() { openProgress = 0; @@ -415,6 +427,7 @@ protected void writeNetworkData(NBTTagCompound nbt) { nbt.setInteger("scanDistance", scanDistance); nbt.setDouble("scanStepLy", stepLightYears); nbt.setBoolean("scanPassive", passive); + nbt.setBoolean("scanWholeSystem", characteriseWholeSystem); } @Override @@ -439,6 +452,7 @@ protected void readNetworkData(NBTTagCompound nbt) { scanDistance = Math.max(1, nbt.getInteger("scanDistance")); stepLightYears = nbt.getDouble("scanStepLy"); passive = nbt.getBoolean("scanPassive"); + characteriseWholeSystem = !nbt.hasKey("scanWholeSystem") || nbt.getBoolean("scanWholeSystem"); if (world != null && world.isRemote && prevSeed != lastSeed) { zmaster587.advancedRocketry.AdvancedRocketry.proxy.clearObservatoryScrollCache(); @@ -466,6 +480,7 @@ public NBTTagCompound writeToNBT(NBTTagCompound nbt) { nbt.setInteger("scanDirection", scanDirection); nbt.setInteger("scanDistance", scanDistance); nbt.setBoolean("scanPassive", passive); + nbt.setBoolean("scanWholeSystem", characteriseWholeSystem); return nbt; } @@ -485,6 +500,7 @@ public void readFromNBT(NBTTagCompound nbt) { scanDirection = nbt.getInteger("scanDirection"); scanDistance = Math.max(1, nbt.getInteger("scanDistance")); passive = nbt.getBoolean("scanPassive"); + characteriseWholeSystem = !nbt.hasKey("scanWholeSystem") || nbt.getBoolean("scanWholeSystem"); } @@ -733,6 +749,15 @@ public List getModules(int ID, EntityPlayer player) { this, zmaster587.libVulpes.inventory.TextureResources.buttonBuild, LibVulpes.proxy.getLocalizedString("msg.observetory.scan.mode.tooltip"), 40, 18)); + // What a detection is followed up with. An operational choice with a cost, so it is a + // control on the instrument and not a setting in a file: over known sky an operator wants + // every body named, and into sky nobody has visited he wants a list of places to fly to. + modules.add(new ModuleButton(166, 66, 9, + LibVulpes.proxy.getLocalizedString(characteriseWholeSystem + ? "msg.observetory.scan.detail.full" : "msg.observetory.scan.detail.coords"), + this, zmaster587.libVulpes.inventory.TextureResources.buttonBuild, + LibVulpes.proxy.getLocalizedString("msg.observetory.scan.detail.tooltip"), 40, 18)); + modules.add(new ModuleText(8, 116, scanStatusText(), 0x2d2d2d, false)); modules.add(new ModuleText(8, 128, LibVulpes.proxy.getLocalizedString("msg.observetory.scan.keepcrystal"), @@ -862,7 +887,7 @@ public boolean beginPassiveSweep() { if (origin == null) { return false; } - int radius = Math.max(0, ARConfiguration.getCurrentConfig().telescopePassiveRadiusCells); + int radius = Math.max(0, ARConfiguration.getCurrentConfig().telescopePassiveRadiusSteps); RegionScan sweep = buildScan(() -> RegionScan.local(origin, radius, world.getTotalWorldTime(), RegionScan.Tuning.fromConfig())); if (sweep == null) { @@ -892,6 +917,11 @@ public int getScanDistance() { return scanDistance; } + /** Whether a detection is followed all the way to the system's bodies, or only its address. */ + public boolean isCharacterisingWholeSystem() { + return characteriseWholeSystem; + } + /** How far the current aim reaches, in light years, or zero before the server has said. */ public double getAimLightYears() { return scanDistance * stepLightYears; @@ -930,13 +960,16 @@ private static int countObscured(UniverseRegistry registry, GalacticCoord origin return 0; } int obscured = 0; + double limit = TelescopeScan.limitMagnitude(); for (int index = from; index < from + count && index < scan.totalCells(); index++) { - GalacticCoord cell = scan.cellAt(index); - if (!registry.anchorForCell(cell).isPresent()) { - continue; // empty sky is not a hidden sky - } - if (TelescopeScan.isObscured(registry, origin, registry.anchorForCell(cell).get())) { - obscured++; + // The DETECTIONS and not the cells: empty sky is not a hidden sky, and neither is a star + // the instrument never registered. What the operator is being told about is the band in + // between - bright enough to see, too dim through the dust to make anything out. + for (TelescopeScan.Detection hit + : TelescopeScan.detect(registry, scan.cellAt(index), origin, limit)) { + if (TelescopeScan.isObscuredAt(hit.extinctionMagnitudes())) { + obscured++; + } } } return obscured; @@ -1051,7 +1084,8 @@ private void completeRegionScanIfDue() { UniverseRegistry registry = UniverseRegistry.get(world); lastScanObscured += countObscured(registry, origin, activeScan, activeScan.cellsDone(), cells); lastScanDiscoveries += TelescopeScan.resolveBatch(registry, activeScan, - activeScan.cellsDone(), cells, crystal, now, TelescopeScan.dimensionNames(), origin); + activeScan.cellsDone(), cells, crystal, now, TelescopeScan.dimensionNames(), origin, + characteriseWholeSystem); activeScan = instant ? activeScan.completed(now) : activeScan.advanced(now, cells); if (activeScan.isComplete()) { activeScan = null; @@ -1109,6 +1143,9 @@ public void onInventoryButtonPressed(int buttonId) { if (buttonId == 8) { PacketHandler.sendToServer(new PacketMachine(this, PASSIVE_SWEEP)); } + if (buttonId == 9) { + PacketHandler.sendToServer(new PacketMachine(this, TOGGLE_WHOLE_SYSTEM)); + } } @@ -1174,6 +1211,14 @@ else if (id == PICK_DIRECTION || id == PICK_DISTANCE) { player.openGui(LibVulpes.instance, GuiHandler.guiId.MODULARNOINV.ordinal(), getWorld(), pos.getX(), pos.getY(), pos.getZ()); } + else if (id == TOGGLE_WHOLE_SYSTEM) { + characteriseWholeSystem = !characteriseWholeSystem; + markDirty(); + IBlockState st = world.getBlockState(pos); + world.notifyBlockUpdate(pos, st, st, 2); + player.openGui(LibVulpes.instance, GuiHandler.guiId.MODULARNOINV.ordinal(), + getWorld(), pos.getX(), pos.getY(), pos.getZ()); + } else if (id == START_SCAN || id == ABORT_SCAN || id == PASSIVE_SWEEP) { if (id == ABORT_SCAN) { abortRegionScan(); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java index d7f38f4e0..71896e1d5 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java @@ -339,7 +339,8 @@ public Map systemsInRegion(long seed, GalacticCo for (long j = jLo; j <= jHi && !capped; j++) { for (long m = mLo; m <= mHi && !capped; m++) { Optional g = systemForLattice(seed, - Lattice.of(supX, supY, supZ, i, j, m, k, s, local.ownField)); + Lattice.of(supX, supY, supZ, i, j, m, k, s, local.ownField, + local.dilution(), local.material)); if (!g.isPresent()) { continue; } @@ -820,9 +821,37 @@ public double columnDensityBetween(long seed, GalacticCoord from, GalacticCoord @Override public Optional anchorAt(long seed, GalacticCoord cell) { - Optional g = systemForLattice(seed, - latticeAt(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ())); - return g.isPresent() ? Optional.of(g.get().cell) : Optional.empty(); + // The SEAT and not the system: this is the hottest question in the game — every address + // resolution, every descent check and every look of a survey goes through it — and it does + // not need to know what stands at the seat in order to say where the seat is. + return seatForLattice(seed, latticeAt(seed, cell.sectorX(), cell.sectorY(), cell.sectorZ())); + } + + @Override + public List anchorsInTerritory(long seed, GalacticCoord cell, int limit) { + long s = config.minSpacing; + long supX = Math.floorDiv(cell.sectorX(), s); + long supY = Math.floorDiv(cell.sectorY(), s); + long supZ = Math.floorDiv(cell.sectorZ(), s); + LocalField local = localFieldAt(seed, supX, supY, supZ); + int k = local.subdivision; + long seats = (long) k * k * k; + if (k <= 1 || seats > Math.max(1, limit)) { + // Either there is nothing to enumerate, or there is far too much: a cluster nucleus + // divides one territory thousands of ways, and a census of it is not a look through a + // telescope. Sampling it is what a survey has always done there, and it stays a find. + return IGalaxyGenerator.super.anchorsInTerritory(seed, cell, limit); + } + List anchors = new ArrayList<>(); + for (long i = 0; i < k; i++) { + for (long j = 0; j < k; j++) { + for (long m = 0; m < k; m++) { + seatForLattice(seed, Lattice.of(supX, supY, supZ, i, j, m, k, s, local.ownField, + local.dilution(), local.material)).ifPresent(anchors::add); + } + } + } + return anchors; } @Override @@ -868,6 +897,36 @@ private static long clampAxis(long sector, long low, long edge) { return sector > hi ? hi : sector; } + /** + * WHERE a lattice cell's system sits, without working out what it is — the occupancy draws and + * the seat, and not one body, star or name. + * + *

    This is the difference between asking "is anything there" and "what is there", and it is + * the same split the survey is built on one layer up. Every draw below decides the seat by the + * cell's own hash, and none of them depends on what the system turns out to BE — a star and an + * unbound world seated in the same cube sit in the same place — so the answer here is exactly + * the cell {@link #systemForLattice} would report, at a fraction of the cost. Fabricating a + * system means drawing its type, its bulk and its companions, and a survey that fabricated one + * per seat it merely walked past spent nine tenths of its time on systems it then discarded.

    + */ + private Optional seatForLattice(long seed, Lattice lattice) { + double bound = Math.max(lattice.material.bound, lattice.ownField); + if (bound > 0d && CellHash.norm(lattice.hash(seed, SALT_OCC)) + < Math.min(1d, config.density * bound / lattice.dilution)) { + return Optional.of(seatIn(seed, lattice)); + } + double profile = Math.max(lattice.material.total(), lattice.ownField); + if (!(profile > 0d)) { + return Optional.empty(); + } + double occupancy = Math.min(1d, + config.density * config.rogue.abundance * profile / lattice.dilution); + if (CellHash.norm(lattice.hash(seed, SALT_ROGUE_OCC)) >= occupancy) { + return Optional.empty(); + } + return Optional.of(seatIn(seed, lattice)); + } + /** The single system a lattice cell hosts (its cell coordinate + fabricated system), or empty. */ private Optional systemForLattice(long seed, Lattice lattice) { // OCCUPANCY IS DECIDED IN THE GALAXY'S OWN FRAME, so the profile does the drawing: the disc, @@ -878,17 +937,19 @@ private Optional systemForLattice(long seed, Lattice lattice) { // the probability a cube is occupied cannot depend on where its seat would have landed. And // evaluated at t = 0 and never again: a time-dependent occupancy would pop systems in and out // of existence. Systems drift afterwards at their galaxy's own omega(r), which is the shear. - GalaxyField.Material material = galaxies.materialAtSector(seed, - lattice.lowX + lattice.edgeX / 2L, lattice.lowY + lattice.edgeY / 2L, - lattice.lowZ + lattice.edgeZ / 2L); + GalaxyField.Material material = lattice.material; // A cluster out in the void supplies its own field, because k³ times the halo is still nothing // and an intergalactic globular has to be a globular. Inside a galaxy ownField is zero and the // profile speaks, so this is the same number it always was everywhere anything already exists. double bound = Math.max(material.bound, lattice.ownField); // Keyed by the cell's LOW CORNER, which is globally unique whatever lattice it belongs to — // a coarse index would collide with a fine one wherever a cluster refines the field. + // ONE SUB-SEAT'S SHARE, not the territory's. Every territory is divided uniformly, so what + // is drawn here is its k-cubed-th part; summed back over the seats it is the same field at + // the same mean separation, and the only thing that has moved is the texture. boolean star = bound > 0d - && CellHash.norm(lattice.hash(seed, SALT_OCC)) < Math.min(1d, config.density * bound); + && CellHash.norm(lattice.hash(seed, SALT_OCC)) + < Math.min(1d, config.density * bound / lattice.dilution); if (!star) { // THE SECOND DRAW, on the cube the first one passed over. Stars need a galaxy to form in; // an unbound world does not, so out in the void this is the only roll there is, and inside @@ -935,7 +996,10 @@ private Optional rogueForLattice(long seed, Lattice lattice, double p if (!(profile > 0d)) { return Optional.empty(); // a galaxy cell with no galaxy in it: the deepest void, and empty } - double occupancy = Math.min(1d, config.density * config.rogue.abundance * profile); + // The whole point of the division: this number saturated at exactly 1.000000 before the + // territory was divided, and the measured abundance of 21 was indistinguishable from 3. + double occupancy = Math.min(1d, + config.density * config.rogue.abundance * profile / lattice.dilution); if (CellHash.norm(lattice.hash(seed, SALT_ROGUE_OCC)) >= occupancy) { return Optional.empty(); } @@ -1020,8 +1084,21 @@ private static final class Lattice { /** See {@link LocalField#ownField} — what a cluster out in the void brings with it. */ final double ownField; + /** + * What share of its territory's occupancy this cell draws for — {@link LocalField#dilution()}. + * + *

    It rides on the cell rather than being looked up at the draw, because the draw happens in + * {@code systemForLattice}, which is handed a cell and nothing else. A cell that did not know + * how finely its own territory was divided would have to ask the field again for a fact the + * partition already decided, and the two answers could differ at the clamp.

    + */ + final double dilution; + + /** The material of the TERRITORY this cell belongs to — see {@link LocalField#material}. */ + final GalaxyField.Material material; + private Lattice(long lowX, long lowY, long lowZ, long edgeX, long edgeY, long edgeZ, - double ownField) { + double ownField, double dilution, GalaxyField.Material material) { this.lowX = lowX; this.lowY = lowY; this.lowZ = lowZ; @@ -1029,11 +1106,13 @@ private Lattice(long lowX, long lowY, long lowZ, long edgeX, long edgeY, long ed this.edgeY = edgeY; this.edgeZ = edgeZ; this.ownField = ownField; + this.dilution = dilution; + this.material = material; } /** Sub-cell {@code (i, j, m)} of coarse super-cell {@code (supX, supY, supZ)}, at {@code k}. */ static Lattice of(long supX, long supY, long supZ, long i, long j, long m, int k, long s, - double ownField) { + double ownField, double dilution, GalaxyField.Material material) { long baseX = supX * s; long baseY = supY * s; long baseZ = supZ * s; @@ -1043,7 +1122,8 @@ static Lattice of(long supX, long supY, long supZ, long i, long j, long m, int k return new Lattice(baseX + loI, baseY + loJ, baseZ + loM, Math.max(1L, Math.floorDiv((i + 1L) * s, (long) k) - loI), Math.max(1L, Math.floorDiv((j + 1L) * s, (long) k) - loJ), - Math.max(1L, Math.floorDiv((m + 1L) * s, (long) k) - loM), ownField); + Math.max(1L, Math.floorDiv((m + 1L) * s, (long) k) - loM), ownField, dilution, + material); } /** Its draw for one field, keyed by the low corner — globally unique at any subdivision. */ @@ -1081,10 +1161,18 @@ long minEdge() { */ private static final class LocalField { - static final LocalField PLAIN = new LocalField(1, 0d); - - /** {@code 1} in the ordinary field, and the covering cluster's {@code k} where there is one. */ + /** How finely the field is divided, all the way down: the UNIFORM division times a cluster's. */ final int subdivision; + /** + * The uniform division ALONE — the {@code k} every territory is divided by whether or not a + * cluster covers it, and therefore the number both occupancies are diluted by. + * + *

    It is carried rather than recomputed because it can be CLAMPED: a coarse cell too small + * to divide keeps a coarser lattice, and diluting by a division that did not happen would + * empty the sky by a factor of twenty-seven. The dilution and the division are one decision, + * so they travel together.

    + */ + final int uniform; /** * The field a cluster BRINGS with it, or zero where the surrounding profile already speaks. * @@ -1096,9 +1184,29 @@ private static final class LocalField { */ final double ownField; - LocalField(int subdivision, double ownField) { + /** + * The galaxy's material at this TERRITORY's centre — what decides how much of it is occupied. + * + *

    Read once per territory and shared by every sub-seat inside it, and that is a statement + * about the model rather than a saving. The profile it comes from varies on the scale of a + * galaxy's disc, thousands of light years; a territory is three. Sampling it per sub-seat + * asked a smooth function twenty-seven times for the same answer — and it cost a survey a + * factor of twenty-seven on the one path a player waits for. What the original comment on + * this draw actually required is that the sampling point be fixed by the PARTITION rather + * than by where a seat would have landed, and a territory's centre is exactly that.

    + */ + final GalaxyField.Material material; + + LocalField(int subdivision, int uniform, double ownField, GalaxyField.Material material) { this.subdivision = subdivision; + this.uniform = uniform; this.ownField = ownField; + this.material = material; + } + + /** What one sub-seat's share of the territory's occupancy is: {@code uniform^3}. */ + double dilution() { + return (double) uniform * uniform * uniform; } } @@ -1112,23 +1220,76 @@ private static final class LocalField { */ private static final double INTERGALACTIC_CLUSTER_FIELD = 1d; + /** + * How finely EVERY star territory is divided, before any cluster refines it further — the uniform + * lattice a free-floating population needs to be counted on. + * + *

    Derived from the rogue abundance, because that is the quantity that could not be represented + * without it. An abundance is a NUMBER DENSITY: so many unbound worlds per star. Mapping it onto + * the star lattice as an occupancy PROBABILITY bounded it at one, so every abundance past + * {@code 1/density} = 2.86 was unrepresentable and the measured 21 saturated the lattice to + * exactly 1.000000 — 21 was indistinguishable from 3, and from 300. Dividing the territory + * {@code k = ceil(abundance^(1/3))} ways per axis gives {@code k^3} seats each holding + * {@code density*abundance/k^3}, and the number is legible again: 0.272 per sub-seat, a mean of + * 7.35 per territory at the shipped abundance.

    + * + *

    The division is uniform — stars included — and both occupancies are diluted by + * {@code k^3}. Dividing only the unbound draw would put up to {@code k^3} anchors in one + * territory, and member-cell attribution would stop being single-valued: two cells of the same + * territory would belong to two different systems, which is what {@code anchorForCell} and every + * address in the game are built on. Dividing everything keeps the invariant's sentence literal — + * one anchor per lattice cell — and leaves the star field's DENSITY untouched: the same + * {@code density} spread over {@code k^3} times as many seats is the same number of stars, at the + * same mean separation, on a finer texture. What it costs is the MINIMUM separation two stars can + * have, which falls from a territory to a sub-cell — and that removes a lattice artefact rather + * than a guarantee, because the floor that matters ({@link UniverseScale#SEPARATION_FLOOR_AU}) + * still has six times the room it needs.

    + * + *

    Capped at {@link #MAX_UNIFORM_SUBDIVISION}, which is what a single telescope look can still + * enumerate — see {@link TelescopeScan#MAX_SEATS_PER_LOOK}. Past that a survey would be back to + * sampling the field, and an abundance nothing can report is no better represented than one + * nothing can store.

    + */ + private int uniformSubdivision() { + double abundance = Math.max(1d, config.rogue.abundance); + long k = (long) Math.ceil(Math.cbrt(abundance)); + return (int) Math.max(1L, Math.min(MAX_UNIFORM_SUBDIVISION, k)); + } + + /** + * The finest uniform division of a star territory, and the reason it is this number and not + * another: {@code 4^3 = 64} is {@link TelescopeScan#MAX_SEATS_PER_LOOK}, the most seats one look + * of a survey will enumerate before it goes back to sampling. The two are the same bound seen + * from the placement side and from the observing side, and neither may move alone. + */ + private static final int MAX_UNIFORM_SUBDIVISION = 4; + private LocalField localFieldAt(long seed, long supX, long supY, long supZ) { long s = config.minSpacing; // The CONTAINING galaxy: a cluster inside a satellite belongs to the satellite, and its nucleus // sits at the satellite's own centre. Absent out in the void, where a cluster may still sit. - Optional galaxy = galaxies.galaxyContainingSector(seed, supX * s + s / 2L, - supY * s + s / 2L, supZ * s + s / 2L); + long centreX = supX * s + s / 2L; + long centreY = supY * s + s / 2L; + long centreZ = supZ * s + s / 2L; + Optional galaxy = galaxies.galaxyContainingSector(seed, centreX, centreY, centreZ); + GalaxyField.Material material = galaxies.materialAtSector(seed, centreX, centreY, centreZ); Optional cluster = clusters.clusterAt(seed, galaxy.orElse(null), supX, supY, supZ); + // Neither a cluster nor the uniform division can conjure room the coarse cell never had. + // Refining below the smallest cell a system can be more than a lone star in would not make a + // dense field — it would make a field of bare stars, which is the opposite of the thing. A + // spacing too tight to refine is a degenerate galaxy rather than an error, exactly as too + // tight a spacing already is. + long ceiling = Math.max(1L, s / UniverseScale.MIN_LATTICE_EDGE_CELLS); + int uniform = (int) Math.max(1L, Math.min(uniformSubdivision(), ceiling)); if (!cluster.isPresent()) { - return LocalField.PLAIN; + return new LocalField(uniform, uniform, 0d, material); } - // A cluster cannot conjure room its coarse cell never had. Refining below the smallest cell a - // system can be more than a lone star in would not make a dense cluster — it would make a - // field of bare stars, which is the opposite of the thing. A spacing too tight to refine is a - // degenerate galaxy rather than an error, exactly as too tight a spacing already is. - long ceiling = Math.max(1L, s / UniverseScale.MIN_LATTICE_EDGE_CELLS); - int k = (int) Math.max(1L, Math.min(cluster.get().subdivision(), ceiling)); - return new LocalField(k, galaxy.isPresent() ? 0d : INTERGALACTIC_CLUSTER_FIELD); + // The cluster's contrast rides ON TOP of the uniform division: it wants k^3 times the + // density, and it gets it by owning k^3 times as many of the same-sized seats. Multiplying + // rather than replacing is what keeps its contrast the same number it always was. + long k = Math.max(1L, Math.min((long) uniform * cluster.get().subdivision(), ceiling)); + return new LocalField((int) k, uniform, + galaxy.isPresent() ? 0d : INTERGALACTIC_CLUSTER_FIELD, material); } /** The lattice cell a sector triple falls in. */ @@ -1140,12 +1301,14 @@ private Lattice latticeAt(long seed, long sectorX, long sectorY, long sectorZ) { LocalField local = localFieldAt(seed, supX, supY, supZ); int k = local.subdivision; if (k <= 1) { - return Lattice.of(supX, supY, supZ, 0L, 0L, 0L, 1, s, local.ownField); + return Lattice.of(supX, supY, supZ, 0L, 0L, 0L, 1, s, local.ownField, local.dilution(), + local.material); } return Lattice.of(supX, supY, supZ, subIndex(Math.floorMod(sectorX, s), s, k), subIndex(Math.floorMod(sectorY, s), s, k), - subIndex(Math.floorMod(sectorZ, s), s, k), k, s, local.ownField); + subIndex(Math.floorMod(sectorZ, s), s, k), k, s, local.ownField, local.dilution(), + local.material); } /** diff --git a/src/main/java/zmaster587/advancedRocketry/universe/ConeWalk.java b/src/main/java/zmaster587/advancedRocketry/universe/ConeWalk.java new file mode 100644 index 000000000..4581cd10d --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/ConeWalk.java @@ -0,0 +1,319 @@ +package zmaster587.advancedRocketry.universe; + +import net.minecraft.nbt.NBTTagCompound; + +import zmaster587.advancedRocketry.space.GalacticCoord; + +/** + * The patch of sky one pointing covers: a CONE with its apex at the instrument, a direction and a + * half-angle — enumerated as an indexed list of look points so a survey can be walked, paused and + * resumed. + * + *

    Why a cone and not a box. A box of coordinates has no observer: it has two corners and + * no idea where it is being looked at from. Everything awkward about surveying one — what a stride + * means, why a distant region is a different shape of work from a near one — comes from a shape that + * does not know where its viewer stands. A cone has an apex, so "how far along the sight line" and + * "how far off axis" are different questions with different answers, which is what an instrument + * actually distinguishes.

    + * + *

    The walk is shell by shell. Look points sit on a lattice of {@code stride} cells: one + * step along the axis per shell, and inside each shell a disc of the same spacing whose radius grows + * as {@code s·stride·tan(halfAngle)}. So a pointing is narrow near the instrument and wide far away, + * which is the whole geometric content of "a patch of sky" — the same angular patch subtends more + * space the farther out you read it.

    + * + *

    Indexed, not iterated. {@link #lookAt(int)} answers any index without walking the ones + * before it, because a survey outlives the chunk it started in: it stores how many looks it has done + * and resumes there. The per-shell counts are exact — a disc is counted as a disc and not as the + * square around it — so a survey's progress describes the sky it covers rather than the bookkeeping + * around it.

    + * + *

    Immutable. The NBT shape is a same-version save contract: a pointing outlives its chunk.

    + */ +public final class ConeWalk { + + private static final String KEY_APEX = "apex"; + private static final String KEY_DIR_X = "dx"; + private static final String KEY_DIR_Y = "dy"; + private static final String KEY_DIR_Z = "dz"; + private static final String KEY_HALF_ANGLE = "halfAngle"; + private static final String KEY_REACH = "reachCells"; + private static final String KEY_STRIDE = "stride"; + + /** + * The most shells one pointing may hold. + * + *

    A REPRESENTATION bound and not a balance one: every shell owns an entry in the prefix table + * this class builds at construction, so the table is what is being bounded. A million shells is + * already 4 MB of index for a survey whose look count passed what an {@code int} cursor can carry + * long before — the two limits are refused together, and the message says which.

    + */ + private static final int MAX_SHELLS = 1_000_000; + + private final GalacticCoord apex; + private final double dirX; + private final double dirY; + private final double dirZ; + private final double halfAngleRadians; + private final long reachCells; + private final long strideCells; + + /** The disc basis: two unit vectors across the axis, so a shell is enumerated in its own plane. */ + private final double uX; + private final double uY; + private final double uZ; + private final double vX; + private final double vY; + private final double vZ; + + /** {@code shellStart[s]} is the index of shell {@code s}'s first look; the last entry is the total. */ + private final int[] shellStart; + + private ConeWalk(GalacticCoord apex, double dirX, double dirY, double dirZ, + double halfAngleRadians, long reachCells, long strideCells) { + this.apex = apex; + double length = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ); + this.dirX = dirX / length; + this.dirY = dirY / length; + this.dirZ = dirZ / length; + this.halfAngleRadians = halfAngleRadians; + this.reachCells = Math.max(0L, reachCells); + this.strideCells = Math.max(1L, strideCells); + + // A vector not parallel to the axis, chosen by which component of the axis is SMALLEST: any + // fixed helper is parallel to some axis, and the cross product with it degenerates exactly + // there. Picking the smallest component guarantees at least a 1/sqrt(3) separation. + double hx = 0d; + double hy = 0d; + double hz = 0d; + double ax = Math.abs(this.dirX); + double ay = Math.abs(this.dirY); + double az = Math.abs(this.dirZ); + if (ax <= ay && ax <= az) { + hx = 1d; + } else if (ay <= az) { + hy = 1d; + } else { + hz = 1d; + } + double cx = this.dirY * hz - this.dirZ * hy; + double cy = this.dirZ * hx - this.dirX * hz; + double cz = this.dirX * hy - this.dirY * hx; + double cl = Math.sqrt(cx * cx + cy * cy + cz * cz); + this.uX = cx / cl; + this.uY = cy / cl; + this.uZ = cz / cl; + this.vX = this.dirY * uZ - this.dirZ * uY; + this.vY = this.dirZ * uX - this.dirX * uZ; + this.vZ = this.dirX * uY - this.dirY * uX; + + this.shellStart = buildShells(); + } + + /** + * Aim a pointing from {@code apex} along {@code (dirX, dirY, dirZ)}. + * + * @param halfAngleRadians how wide the patch of sky is, from the axis to the edge + * @param reachCells how far the pointing carries — derived from what the instrument can + * SEE (see {@link StellarMagnitude#instrumentReachLightYears}), never a + * horizon of its own + * @param strideCells the spacing of the look lattice — one star's territory + * @throws IllegalArgumentException when there is no apex, no direction, or the pointing holds + * more looks than a survey cursor can index + */ + public static ConeWalk aimed(GalacticCoord apex, double dirX, double dirY, double dirZ, + double halfAngleRadians, long reachCells, long strideCells) { + if (apex == null) { + throw new IllegalArgumentException("a pointing needs an instrument to be aimed from"); + } + if (dirX * dirX + dirY * dirY + dirZ * dirZ <= 0d) { + throw new IllegalArgumentException("a pointing with no direction does not name a patch of sky"); + } + // Clamped rather than refused: an operator who asks for a hemisphere gets the widest patch the + // geometry can mean, and one who asks for zero gets the single sight line, which is a pointing + // with no width and still a pointing. + double half = Math.max(0d, Math.min(Math.PI / 2d - 1e-6d, halfAngleRadians)); + return new ConeWalk(apex.cellCentre(), dirX, dirY, dirZ, half, reachCells, strideCells); + } + + /** + * The prefix table of shell starts — and the place a pointing too large to walk is REFUSED. + * + *

    Refused and never clamped, for the reason a region survey already states: a walk cursor is an + * {@code int}, so a pointing with more looks than one can index would report itself complete with + * most of the sky untouched, and progress would read 100 % over a survey that never happened. + * Silence is the one outcome worse than a slow survey.

    + */ + private int[] buildShells() { + long shells = reachCells / strideCells; + if (shells > MAX_SHELLS) { + throw new IllegalArgumentException("a pointing of " + shells + " shells cannot be walked" + + " (at most " + MAX_SHELLS + "): the instrument reaches " + reachCells + + " cells at a stride of " + strideCells + ". Lower the limiting magnitude."); + } + int n = (int) Math.max(0L, shells); + int[] start = new int[n + 1]; + long total = 0L; + for (int s = 1; s <= n; s++) { + start[s - 1] = (int) total; + total += discLooks(radiusOfShell(s)); + if (total > Integer.MAX_VALUE) { + throw new IllegalArgumentException("a pointing of half-angle " + + String.format("%.3f", Math.toDegrees(halfAngleRadians)) + " degrees over " + + reachCells + " cells holds more looks than a survey can index." + + " Narrow the aperture or lower the limiting magnitude."); + } + } + start[n] = (int) total; + return start; + } + + /** The disc radius of shell {@code s}, in STRIDES — {@code s·tan(halfAngle)}, floored to the lattice. */ + private int radiusOfShell(int s) { + double radius = s * Math.tan(halfAngleRadians); + return (int) Math.max(0d, Math.min(Integer.MAX_VALUE, Math.floor(radius))); + } + + /** How many lattice points a disc of {@code radius} strides holds — counted row by row, exactly. */ + private static long discLooks(int radius) { + long count = 0L; + for (int i = -radius; i <= radius; i++) { + count += 2L * rowHalfWidth(radius, i) + 1L; + } + return count; + } + + /** Half the width of the disc's row at offset {@code i} — {@code floor(sqrt(r² − i²))}. */ + private static int rowHalfWidth(int radius, int i) { + long r2 = (long) radius * radius - (long) i * i; + return r2 <= 0L ? 0 : (int) Math.sqrt((double) r2); + } + + /** How many looks the whole pointing holds. Never a clamped count standing in for a real one. */ + public int totalLooks() { + return shellStart[shellStart.length - 1]; + } + + /** How many shells deep the pointing goes — one step of {@link #strideCells()} each. */ + public int shells() { + return shellStart.length - 1; + } + + public GalacticCoord apex() { + return apex; + } + + public double halfAngleRadians() { + return halfAngleRadians; + } + + /** How far the pointing carries, in cells — the instrument's reach, not a configured horizon. */ + public long reachCells() { + return reachCells; + } + + public long strideCells() { + return strideCells; + } + + /** The unit direction the instrument is aimed along. */ + public double dirX() { + return dirX; + } + + public double dirY() { + return dirY; + } + + public double dirZ() { + return dirZ; + } + + /** + * The cell the look at {@code index} lands on, in the pointing's own order: shell by shell + * outwards, and inside a shell row by row across the disc. + * + *

    Outwards first is not cosmetic. A survey resolves its looks in this order and may be aborted + * at any point, so what a half-finished pointing has covered is a SHORTER cone rather than a + * scatter — the operator has surveyed the near sky and not a random sample of the far.

    + */ + public GalacticCoord lookAt(int index) { + int shell = shellFor(index); + int radius = radiusOfShell(shell); + int offset = index - shellStart[shell - 1]; + // Walk the disc's rows to place the offset. At most 2r+1 steps, against a look that costs a + // lattice draw — the cost is in what the look RESOLVES, never in finding where it points. + int i = -radius; + while (i <= radius) { + int width = 2 * rowHalfWidth(radius, i) + 1; + if (offset < width) { + break; + } + offset -= width; + i++; + } + int j = offset - rowHalfWidth(radius, i); + + double axial = (double) shell * strideCells; + double across = (double) i * strideCells; + double along = (double) j * strideCells; + return GalacticCoord.ofSectorLocal( + apex.sectorX() + Math.round(dirX * axial + uX * across + vX * along), + apex.sectorY() + Math.round(dirY * axial + uY * across + vY * along), + apex.sectorZ() + Math.round(dirZ * axial + uZ * across + vZ * along), + 0L, 0L, 0L); + } + + /** Which shell an index falls in, by binary search over the prefix table. Shells are 1-based. */ + private int shellFor(int index) { + if (index < 0 || index >= totalLooks()) { + throw new IndexOutOfBoundsException("look " + index + " of " + totalLooks()); + } + int lo = 1; + int hi = shells(); + while (lo < hi) { + int mid = (lo + hi + 1) >>> 1; + if (shellStart[mid - 1] <= index) { + lo = mid; + } else { + hi = mid - 1; + } + } + return lo; + } + + /** How far out shell {@code shell} stands, in cells — what a look at that depth costs to resolve. */ + public long axialCellsOfShell(int shell) { + return (long) shell * strideCells; + } + + public void writeToNBT(NBTTagCompound nbt) { + NBTTagCompound at = new NBTTagCompound(); + apex.writeToNBT(at); + nbt.setTag(KEY_APEX, at); + nbt.setDouble(KEY_DIR_X, dirX); + nbt.setDouble(KEY_DIR_Y, dirY); + nbt.setDouble(KEY_DIR_Z, dirZ); + nbt.setDouble(KEY_HALF_ANGLE, halfAngleRadians); + nbt.setLong(KEY_REACH, reachCells); + nbt.setLong(KEY_STRIDE, strideCells); + } + + /** The pointing stored in {@code nbt}, or {@code null} when nothing was stored. */ + public static ConeWalk readFromNBT(NBTTagCompound nbt) { + if (nbt == null || !nbt.hasKey(KEY_APEX)) { + return null; + } + return new ConeWalk(GalacticCoord.readFromNBT(nbt.getCompoundTag(KEY_APEX)), + nbt.getDouble(KEY_DIR_X), nbt.getDouble(KEY_DIR_Y), nbt.getDouble(KEY_DIR_Z), + nbt.getDouble(KEY_HALF_ANGLE), nbt.getLong(KEY_REACH), nbt.getLong(KEY_STRIDE)); + } + + @Override + public String toString() { + return "ConeWalk[" + apex.cellKey() + " -> (" + String.format("%.3f", dirX) + ", " + + String.format("%.3f", dirY) + ", " + String.format("%.3f", dirZ) + "), " + + String.format("%.3f", Math.toDegrees(halfAngleRadians)) + " deg, " + + shells() + " shells, " + totalLooks() + " looks]"; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java index da1e7fb7f..fb8316e8d 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/IGalaxyGenerator.java @@ -107,6 +107,33 @@ default int minSpacingCells() { return GalaxyGenConfig.DEFAULT_MIN_SPACING; } + /** + * Every anchor seated inside the star TERRITORY that {@code cell} falls in — what one look of a + * survey owes the direction it is pointed in. + * + *

    A survey strides by the territory, because that is the cube that holds at most one system + * and walking finer would spend a whole sweep re-reading one system's own neighbourhood. But a + * generator is free to divide that cube further, and then a stride that samples ONE point of it + * reports a fraction of the sky and calls it the sky. So a look asks for the territory's + * contents rather than for the point's, and the resolution of the answer is the generator's own + * business rather than the surveyor's.

    + * + *

    {@code limit} is a refusal, not a truncation. A generator that would return more than + * {@code limit} anchors returns the single anchor at {@code cell} instead — the sampling a + * survey has always done inside a star cluster, where one look is a find and not a census. + * Returning the first {@code limit} of them would be worse than sampling: it would be a biased + * corner of the territory presented as its whole.

    + * + *

    Default: whatever {@link #anchorAt} answers, which is exactly right for a generator whose + * lattice has one seat per territory.

    + */ + default List anchorsInTerritory(long seed, + zmaster587.advancedRocketry.space.GalacticCoord cell, int limit) { + Optional anchor = anchorAt(seed, cell); + return anchor.isPresent() ? Collections.singletonList(anchor.get()) : Collections + .emptyList(); + } + /** * The tunables this generator was built from, when it has any — what a {@code } element * would have to say to reproduce it, and what the save fingerprints so a later load can tell that diff --git a/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java b/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java index 5d623d417..def8b9dfe 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/RegionScan.java @@ -6,22 +6,30 @@ import zmaster587.advancedRocketry.space.GalacticCoord; /** - * A telescope's survey of one region of the galaxy: which box of cells it covers, how far through - * it the instrument has got, and when the next batch of cells is resolved. + * A telescope's survey: which sky it covers, how far through it the instrument has got, and when the + * next batch of looks is resolved. * - *

    A survey sweeps. It walks its region a cell at a time, a bounded number of cells per - * step, writing what each one holds as it goes — an operator points the instrument at a patch of sky - * once and the machine works through it, rather than being re-aimed by hand for every cell. That - * bound is what keeps a procedurally endless universe from being enumerated in a tick; the reach - * bound keeps the patch inside the local cluster.

    + *

    A survey sweeps. It walks its looks a bounded number at a time, writing what each one + * holds as it goes — an operator points the instrument at a patch of sky once and the machine works + * through it, rather than being re-aimed by hand for every cell. That bound is what keeps a + * procedurally endless universe from being enumerated in a tick.

    * - *

    It samples, it does not enumerate. Between two cells it looks at lies a whole star's - * territory — the survey strides by {@link Tuning#strideCells()}, which is the edge of the cube that - * holds at most one system. Walking cell by cell would spend a whole sweep re-reading one system's - * own neighbourhood, since every cell of a system's territory resolves to that same system; striding - * by the territory means a sweep of N cells looks at N candidate systems. In a star CLUSTER, where - * the lattice is subdivided below that edge, a survey therefore samples the cluster rather than - * emptying it: what is inside one stride and off the sampled cell is left for another look.

    + *

    Two shapes, and they are two different instruments.

    + *
      + *
    • A pointing ({@link #directed}) is a {@link ConeWalk}: an apex at the observatory, a + * direction, a half-angle, and a reach that comes from what the instrument can SEE rather than + * from a configured horizon. This is the telescope.
    • + *
    • A local radar ({@link #local}) is a box of the star territories around the + * observatory's own. This is the passive watch over the neighbourhood: near sky, no aiming, and + * its data ready.
    • + *
    + * + *

    It samples, it does not enumerate. Between two looks of a pointing lies a whole star's + * territory — the sweep strides by {@link Tuning#strideCells()}, the edge of the cube that holds at + * most one system. Walking cell by cell would spend a whole sweep re-reading one system's own + * neighbourhood, since every cell of a system's territory resolves to that same system. What a look + * OWES its territory — one seat, or all of the sub-seats a cluster divides it into — is the resolving + * side's business and lives in {@link TelescopeScan}, not here.

    * *

    Each step is a deadline, never a counter: the tick the next batch lands is stored, so a * survey whose observatory unloads mid-sweep resumes exactly where it stood, owing no replay.

    @@ -40,7 +48,10 @@ public final class RegionScan { private static final String KEY_CELLS_DONE = "cellsDone"; private static final String KEY_CELLS_PER_STEP = "cellsPerStep"; private static final String KEY_TICKS_PER_STEP = "ticksPerStep"; + private static final String KEY_CONE = "cone"; + /** The pointing this survey walks, or {@code null} for the box-shaped local radar. */ + private final ConeWalk cone; private final GalacticCoord min; private final GalacticCoord max; private final long distanceCells; @@ -52,11 +63,10 @@ public final class RegionScan { private final int ticksPerStep; private final int totalCells; - private RegionScan(GalacticCoord min, GalacticCoord max, long distanceCells, long strideCells, - long startTick, long stepDeadline, int cellsDone, int cellsPerStep, - int ticksPerStep) { - this.min = min; - this.max = max; + private RegionScan(ConeWalk cone, GalacticCoord min, GalacticCoord max, long distanceCells, + long strideCells, long startTick, long stepDeadline, int cellsDone, + int cellsPerStep, int ticksPerStep) { + this.cone = cone; this.distanceCells = Math.max(0L, distanceCells); this.strideCells = Math.max(1L, strideCells); this.startTick = startTick; @@ -64,7 +74,21 @@ private RegionScan(GalacticCoord min, GalacticCoord max, long distanceCells, lon this.cellsDone = cellsDone; this.cellsPerStep = Math.max(1, cellsPerStep); this.ticksPerStep = Math.max(0, ticksPerStep); - this.totalCells = countLooks(min, max, this.strideCells); + if (cone == null) { + this.min = min; + this.max = max; + this.totalCells = countLooks(min, max, this.strideCells); + } else { + // The corners a cone reports are its BOUNDING BOX and nothing it promises to fill: they + // exist because a survey is asked "roughly where are you looking" by the status read-out + // and by the obscured-count probe, and a cone has no corners of its own to answer with. + long reach = cone.reachCells(); + this.min = GalacticCoord.ofSectorLocal(cone.apex().sectorX() - reach, + cone.apex().sectorY() - reach, cone.apex().sectorZ() - reach, 0L, 0L, 0L); + this.max = GalacticCoord.ofSectorLocal(cone.apex().sectorX() + reach, + cone.apex().sectorY() + reach, cone.apex().sectorZ() + reach, 0L, 0L, 0L); + this.totalCells = cone.totalLooks(); + } } /** @@ -95,101 +119,91 @@ private static int countLooks(GalacticCoord min, GalacticCoord max, long stride) } /** - * Aim a survey from {@code origin} along a direction, {@code distanceSteps} star territories out. + * Point the instrument from {@code origin} along a direction, {@code distanceSteps} star + * territories deep. + * + *

    Unlike a box, a pointing keeps its direction EXACTLY: the vector is used as given rather + * than reduced to a sign per axis, because a cone that snapped to the twenty-six lattice + * directions would not be an aim, it would be a menu.

    * - *

    The direction is taken as a sign per axis, so any vector pointing the same way aims the same - * survey. The distance is counted in STEPS — one step is one star's territory, the same stride - * the sweep walks by — so an aim of 3 means "three stars out", not three cells, which would be a - * fraction of one system. It is clamped into {@code [1, maxRangeSteps]} rather than refused: an - * operator who asks for more than the instrument can reach gets the instrument's reach, which is - * what a horizon means.

    + *

    The depth is counted in STEPS — one step is one star's territory, the same stride the sweep + * walks by — and is clamped into {@code [1, maxRangeSteps]} rather than refused: an operator who + * asks for more than the instrument can reach gets the instrument's reach, which is what a horizon + * means. That reach is {@link Tuning#maxRangeSteps()}, which is derived from the aperture's + * limiting magnitude and is a fact about the instrument rather than a number someone set.

    * * @throws IllegalArgumentException if there is no origin, or the direction is the zero vector — - * a survey with no direction does not name a region. + * a pointing with no direction does not name a patch of sky. */ public static RegionScan directed(GalacticCoord origin, int dirX, int dirY, int dirZ, int distanceSteps, long startTick, Tuning tuning) { if (origin == null) { - throw new IllegalArgumentException("a region survey needs an origin to aim from"); + throw new IllegalArgumentException("a survey needs an origin to aim from"); } if (tuning == null) { - throw new IllegalArgumentException("a region survey needs its bounds"); + throw new IllegalArgumentException("a survey needs its bounds"); } - int dx = Integer.signum(dirX); - int dy = Integer.signum(dirY); - int dz = Integer.signum(dirZ); - if (dx == 0 && dy == 0 && dz == 0) { - throw new IllegalArgumentException("a survey with no direction does not name a region"); - } - long stride = tuning.strideCells(); int steps = Math.max(1, Math.min(distanceSteps, tuning.maxRangeSteps())); - long distance = steps * stride; - long half = tuning.effectiveHalfWidthSteps() * stride; - - long cx = origin.sectorX() + (long) dx * distance; - long cy = origin.sectorY() + (long) dy * distance; - long cz = origin.sectorZ() + (long) dz * distance; - - return new RegionScan( - GalacticCoord.ofSectorLocal(cx - half, cy - half, cz - half, 0L, 0L, 0L), - GalacticCoord.ofSectorLocal(cx + half, cy + half, cz + half, 0L, 0L, 0L), - distance, stride, startTick, startTick + stepTicks(distance, tuning), - 0, tuning.cellsPerStep(), stepTicks(distance, tuning)); + ConeWalk aimed = tuning.fit(origin, dirX, dirY, dirZ, steps); + int ticks = tuning.baseTicks(); + return new RegionScan(aimed, null, null, aimed.reachCells(), stride, startTick, + startTick + ticks, 0, tuning.cellsPerStep(), ticks); } /** - * The passive local radar: a box of {@code radiusCells} cells around {@code origin}, walked cell - * by cell. + * The passive local radar: the observatory's own star territory and the {@code radiusSteps} rings + * of territories around it. * - *

    Its stride is ONE CELL and that is deliberate — this is a radar over the observatory's own - * neighbourhood, where the cells really are the interesting granularity (the planet in the next - * cell over is a different destination from its star). The directed survey is the one that looks - * far away, and it is the one that strides by star territories.

    + *

    Territories and not cells. This walked cell by cell until 2026-08-19, on the ground + * that "the planet in the next cell over is a different destination from its star" — which is + * true and is not a reason, because one look already yields every body of the system that owns + * it. What a cell-by-cell radius bought was nothing at all: two cells is 0.107 AU, a fifth of the + * way to the innermost planet of the system the instrument is already standing in, and no radius + * a cell-strided box could afford would ever have reached a NEIGHBOUR, which is a whole territory + * away. A radius of one territory is twenty-seven looks and is what "watches the neighbourhood" + * was always meant to say.

    */ - public static RegionScan local(GalacticCoord origin, int radiusCells, long startTick, + public static RegionScan local(GalacticCoord origin, int radiusSteps, long startTick, Tuning tuning) { if (origin == null) { throw new IllegalArgumentException("a local radar needs the cell it is standing in"); } if (tuning == null) { - throw new IllegalArgumentException("a region survey needs its bounds"); + throw new IllegalArgumentException("a survey needs its bounds"); } - long radius = Math.max(0, radiusCells); - int ticks = stepTicks(radius, tuning); - return new RegionScan( + long stride = tuning.strideCells(); + long radius = Math.max(0, radiusSteps) * stride; + int ticks = tuning.baseTicks(); + return new RegionScan(null, GalacticCoord.ofSectorLocal(origin.sectorX() - radius, origin.sectorY() - radius, origin.sectorZ() - radius, 0L, 0L, 0L), GalacticCoord.ofSectorLocal(origin.sectorX() + radius, origin.sectorY() + radius, origin.sectorZ() + radius, 0L, 0L, 0L), - radius, 1L, startTick, startTick + ticks, 0, tuning.cellsPerStep(), ticks); + radius, stride, startTick, startTick + ticks, 0, tuning.cellsPerStep(), ticks); } - /** - * What one step of a survey aimed {@code distanceCells} away costs, in ticks: a fixed cost for - * holding the instrument on a patch of sky at all, plus a price per light year of distance. - * - *

    The distance is converted to light years before it is priced, so what a far look costs stays - * put when the cell edge or the star spacing is retuned.

    - */ - private static int stepTicks(long distanceCells, Tuning tuning) { - double ticks = tuning.baseTicks() - + tuning.ticksPerLightYear() - * UniverseRegistry.getGenerator().laws().lightYearsForCells(distanceCells); - return (int) Math.max(0L, Math.min(Integer.MAX_VALUE, Math.round(ticks))); + /** The pointing this survey walks, or {@code null} when it is the box-shaped local radar. */ + public ConeWalk cone() { + return cone; } - /** The inclusive low corner of the surveyed sector box. */ + /** {@code true} when this survey is a pointing rather than the local radar. */ + public boolean isPointing() { + return cone != null; + } + + /** The inclusive low corner of the surveyed box; for a pointing, of the box that bounds it. */ public GalacticCoord min() { return min; } - /** The inclusive high corner of the surveyed sector box. */ + /** The inclusive high corner of the surveyed box; for a pointing, of the box that bounds it. */ public GalacticCoord max() { return max; } - /** How far out the survey was aimed, in cells, after the range clamp. */ + /** How far out the survey reaches, in cells, after the range clamp. */ public long distanceCells() { return distanceCells; } @@ -208,12 +222,12 @@ public long startTick() { return startTick; } - /** The tick the next batch of cells is resolved. */ + /** The tick the next batch of looks is resolved. */ public long stepDeadline() { return stepDeadline; } - /** How many cells of the region have been resolved so far. */ + /** How many looks of the survey have been resolved so far. */ public int cellsDone() { return cellsDone; } @@ -227,9 +241,10 @@ public int ticksPerStep() { } /** - * How many cells this survey LOOKS at — not how many the region contains. The two differ by the - * stride: a region a hundred territories wide is a hundred looks, not a hundred million cells. - * Bounded at construction; never unbounded, and never a clamped count standing in for a real one. + * How many cells this survey LOOKS at — not how many the sky it covers contains. The two differ + * by the stride: a pointing a hundred territories deep is a few thousand looks, not the hundreds + * of millions of cells the cone encloses. Bounded at construction; never unbounded, and never a + * clamped count standing in for a real one. */ public int totalCells() { return totalCells; @@ -245,7 +260,7 @@ private long countAlong(long lo, long hi) { return countAlong(lo, hi, strideCells); } - /** {@code true} once every cell of the region has been resolved. */ + /** {@code true} once every look of the survey has been resolved. */ public boolean isComplete() { return cellsDone >= totalCells(); } @@ -255,7 +270,7 @@ public boolean stepDue(long now) { return !isComplete() && now >= stepDeadline; } - /** How much of the region is surveyed, in {@code [0,1]}. Cells resolved, not ticks elapsed. */ + /** How much of the survey is done, in {@code [0,1]}. Looks resolved, not ticks elapsed. */ public float progress() { int total = totalCells(); if (total <= 0) { @@ -264,17 +279,24 @@ public float progress() { return Math.min(1f, cellsDone / (float) total); } - /** Roughly how long the whole sweep takes — what a farther region costs against a nearer one. */ + /** Roughly how long the whole sweep takes — what a deeper pointing costs against a shallower one. */ public long estimatedTicks() { int steps = (totalCells() + cellsPerStep - 1) / cellsPerStep; return (long) steps * ticksPerStep; } /** - * The cell at {@code index} in the sweep order: rows along X, then Z, then Y, a stride apart. The - * order is deterministic so a resumed sweep continues where it stopped rather than starting over. + * The cell the look at {@code index} lands on. + * + *

    For a pointing, the cone's own order: shell by shell outwards, so an aborted survey has + * covered a SHORTER cone rather than a scatter. For the local radar, rows along X, then Z, then Y, + * a stride apart. Both are deterministic, so a resumed sweep continues where it stopped rather + * than starting over.

    */ public GalacticCoord cellAt(int index) { + if (cone != null) { + return cone.lookAt(index); + } long width = countAlong(min.sectorX(), max.sectorX()); long depth = countAlong(min.sectorZ(), max.sectorZ()); long perLayer = width * depth; @@ -286,7 +308,22 @@ public GalacticCoord cellAt(int index) { min.sectorY() + y * strideCells, min.sectorZ() + z * strideCells, 0L, 0L, 0L); } - /** How many cells the batch due at {@code now} covers — the per-step bound, or what is left. */ + /** + * Where the survey is looking FROM — the apex of a pointing, or the centre of the radar's box. + * + *

    The resolving side needs it for something the box shape never had to answer: how far away + * what it just found is, which is half of how bright the thing looks.

    + */ + public GalacticCoord observer() { + if (cone != null) { + return cone.apex(); + } + return GalacticCoord.ofSectorLocal((min.sectorX() + max.sectorX()) / 2L, + (min.sectorY() + max.sectorY()) / 2L, (min.sectorZ() + max.sectorZ()) / 2L, + 0L, 0L, 0L); + } + + /** How many looks the batch due at {@code now} covers — the per-step bound, or what is left. */ public int cellsDueAt(long now) { if (!stepDue(now)) { return 0; @@ -294,27 +331,33 @@ public int cellsDueAt(long now) { return Math.min(cellsPerStep, totalCells() - cellsDone); } - /** The survey after a batch of {@code resolved} cells has been written, with its next deadline. */ + /** The survey after a batch of {@code resolved} looks has been written, with its next deadline. */ public RegionScan advanced(long now, int resolved) { int done = Math.min(totalCells(), cellsDone + Math.max(0, resolved)); - return new RegionScan(min, max, distanceCells, strideCells, startTick, now + ticksPerStep, - done, cellsPerStep, ticksPerStep); + return new RegionScan(cone, min, max, distanceCells, strideCells, startTick, + now + ticksPerStep, done, cellsPerStep, ticksPerStep); } - /** The survey with every cell resolved — the instant path, where time is not the mechanic. */ + /** The survey with every look resolved — the instant path, where time is not the mechanic. */ public RegionScan completed(long now) { - return new RegionScan(min, max, distanceCells, strideCells, startTick, now, totalCells(), - cellsPerStep, ticksPerStep); + return new RegionScan(cone, min, max, distanceCells, strideCells, startTick, now, + totalCells(), cellsPerStep, ticksPerStep); } public void writeToNBT(NBTTagCompound nbt) { - NBTTagCompound lo = new NBTTagCompound(); - min.writeToNBT(lo); - nbt.setTag(KEY_MIN, lo); - - NBTTagCompound hi = new NBTTagCompound(); - max.writeToNBT(hi); - nbt.setTag(KEY_MAX, hi); + if (cone != null) { + NBTTagCompound aim = new NBTTagCompound(); + cone.writeToNBT(aim); + nbt.setTag(KEY_CONE, aim); + } else { + NBTTagCompound lo = new NBTTagCompound(); + min.writeToNBT(lo); + nbt.setTag(KEY_MIN, lo); + + NBTTagCompound hi = new NBTTagCompound(); + max.writeToNBT(hi); + nbt.setTag(KEY_MAX, hi); + } nbt.setLong(KEY_DISTANCE, distanceCells); nbt.setLong(KEY_STRIDE, strideCells); @@ -327,10 +370,23 @@ public void writeToNBT(NBTTagCompound nbt) { /** The survey stored in {@code nbt}, or {@code null} when nothing was stored. */ public static RegionScan readFromNBT(NBTTagCompound nbt) { - if (nbt == null || !nbt.hasKey(KEY_MIN) || !nbt.hasKey(KEY_MAX)) { + if (nbt == null) { + return null; + } + if (nbt.hasKey(KEY_CONE)) { + return new RegionScan(ConeWalk.readFromNBT(nbt.getCompoundTag(KEY_CONE)), null, null, + nbt.getLong(KEY_DISTANCE), + nbt.getLong(KEY_STRIDE), + nbt.getLong(KEY_START), + nbt.getLong(KEY_STEP_DEADLINE), + nbt.getInteger(KEY_CELLS_DONE), + nbt.getInteger(KEY_CELLS_PER_STEP), + nbt.getInteger(KEY_TICKS_PER_STEP)); + } + if (!nbt.hasKey(KEY_MIN) || !nbt.hasKey(KEY_MAX)) { return null; } - return new RegionScan( + return new RegionScan(null, GalacticCoord.readFromNBT(nbt.getCompoundTag(KEY_MIN)), GalacticCoord.readFromNBT(nbt.getCompoundTag(KEY_MAX)), nbt.getLong(KEY_DISTANCE), @@ -344,36 +400,48 @@ public static RegionScan readFromNBT(NBTTagCompound nbt) { @Override public String toString() { + if (cone != null) { + return "RegionScan[" + cone + ", " + cellsDone + "/" + totalCells() + + " looks, next@" + stepDeadline + "]"; + } return "RegionScan[" + min.cellKey() + " .. " + max.cellKey() + ", " + cellsDone + "/" + totalCells() + " cells, next@" + stepDeadline + "]"; } /** - * What bounds a survey and what it costs in time. Every number here is balance, not contract: the - * reach, the size of the patch, how many cells one step resolves and how long a step takes. + * What bounds a survey and what it costs in time. * - *

    The reach is stated as a LENGTH — light years, the unit a telescope's horizon is quoted in — - * and converted here against the stride. Stating it as a count of anything would make the - * instrument's horizon move whenever the star spacing or the cell edge was retuned, which is how - * a reach came to mean a fifth of the way to Mercury.

    + *

    The reach is not here, and that is the point of the shape: an instrument reaches a + * BRIGHTNESS, and how far that carries is derived from the aperture's limiting magnitude against + * the brightest star the galaxy can produce ({@link StellarMagnitude#instrumentReachLightYears}). + * A configured length was the wrong quantity — it made one number stand for a red dwarf and a blue + * giant, whose ranges differ by eighty times, and it moved whenever the star spacing or the cell + * edge was retuned. What remains configurable is the aperture, the width of the patch, and the + * cost in time; those are balance, never contract.

    */ public static final class Tuning { - private final double maxRangeLightYears; - private final int halfWidthSteps; + private final double limitMagnitude; + private final double reachLightYears; + private final double halfAngleRadians; private final int maxCells; private final int baseTicks; - private final double ticksPerLightYear; private final int cellsPerStep; private final long strideCells; - public Tuning(double maxRangeLightYears, int halfWidthSteps, int maxCells, int baseTicks, - double ticksPerLightYear, int cellsPerStep, long strideCells) { - this.maxRangeLightYears = Math.max(0d, maxRangeLightYears); - this.halfWidthSteps = Math.max(0, halfWidthSteps); + /** + * @param archetypes the star types the sky can produce — the reach is DERIVED against the + * brightest of them here and is never a field anyone can set, so an + * instrument's horizon cannot disagree with its aperture + */ + public Tuning(double limitMagnitude, Iterable archetypes, + double halfAngleRadians, int maxCells, int baseTicks, int cellsPerStep, + long strideCells) { + this.limitMagnitude = limitMagnitude; + this.reachLightYears = StellarMagnitude.instrumentReachLightYears(archetypes, limitMagnitude); + this.halfAngleRadians = Math.max(0d, halfAngleRadians); this.maxCells = Math.max(1, maxCells); this.baseTicks = Math.max(0, baseTicks); - this.ticksPerLightYear = Math.max(0d, ticksPerLightYear); this.cellsPerStep = Math.max(1, cellsPerStep); this.strideCells = Math.max(1L, strideCells); } @@ -386,21 +454,44 @@ public Tuning(double maxRangeLightYears, int halfWidthSteps, int maxCells, int b public static Tuning fromConfig() { ARConfiguration config = ARConfiguration.getCurrentConfig(); return new Tuning( - config.telescopeScanRangeLightYears, - config.telescopeScanHalfWidthSteps, + config.telescopeLimitingMagnitude, + // The STOCK sky when the installed generator describes none of its own. A + // generator with no star table has not said the sky is empty - it has said it + // does not place stars, and an authored pack's suns are real light an instrument + // has to be able to reach. Falling back to the reference table is the same move + // as reading an unstated bulk as one Earth; taking the empty list literally gave + // the instrument a reach of zero and collapsed every pointing to a single shell. + UniverseRegistry.getGenerator().tuning() + .map(c -> c.starTypes) + .filter(types -> !types.isEmpty()) + .orElse(GalaxyGenConfig.defaults().starTypes), + Math.toRadians(config.telescopeConeHalfAngleDegrees), config.telescopeScanMaxCells, config.telescopeScanBaseTicks, - config.telescopeScanTicksPerLightYear, config.telescopeScanCellsPerStep, UniverseRegistry.getGenerator().minSpacingCells()); } - /** The instrument's horizon, as a length. */ + /** How faint a star this instrument can still register. Magnitudes: larger is fainter. */ + public double limitMagnitude() { + return limitMagnitude; + } + + /** How wide a patch of sky one pointing covers, from its axis to its edge. */ + public double halfAngleRadians() { + return halfAngleRadians; + } + + /** + * The instrument's horizon, as a length — DERIVED from the limiting magnitude against the + * brightest archetype the active generator can produce, and zero for a generator that + * produces no stars at all. + */ public double maxRangeLightYears() { - return maxRangeLightYears; + return reachLightYears; } - /** How far apart the cells a directed survey looks at stand — one star's territory. */ + /** How far apart the cells a pointing looks at stand — one star's territory. */ public long strideCells() { return strideCells; } @@ -408,7 +499,7 @@ public long strideCells() { /** The horizon as a number of steps, which is what an operator aims in. At least one. */ public int maxRangeSteps() { long steps = UniverseRegistry.getGenerator().laws() - .cellsForLightYears(maxRangeLightYears) / strideCells; + .cellsForLightYears(maxRangeLightYears()) / strideCells; return (int) Math.max(1L, Math.min(Integer.MAX_VALUE, steps)); } @@ -416,30 +507,52 @@ public int baseTicks() { return baseTicks; } - public double ticksPerLightYear() { - return ticksPerLightYear; - } - public int cellsPerStep() { return cellsPerStep; } + /** The hard ceiling on how many looks one survey may hold. */ + public int maxCells() { + return maxCells; + } + /** - * The half-width a survey actually gets, in steps: the configured one, narrowed until the - * number of cells it would look at fits inside the ceiling. The ceiling wins over the width — - * a sweep may be long, but it may not be unbounded. + * The deepest pointing of {@code steps} that still fits under {@link #maxCells()} — SHORTENED + * rather than refused, exactly as a box survey's width used to be narrowed. + * + *

    The ceiling wins over the depth. A survey may be long, but it may not be unbounded, and a + * pointing that will not fit is one an operator gets less of rather than none of: he sees the + * near sky and can point again. Halving is used rather than decrementing because the look + * count grows as the cube of the depth — walking down one step at a time from a magnitude + * limit that reaches a hundred thousand steps would be the same unbounded work in a + * different place.

    */ - public int effectiveHalfWidthSteps() { - int half = halfWidthSteps; - while (half > 0 && volumeOf(half) > maxCells) { - half--; + public ConeWalk fit(GalacticCoord origin, int dirX, int dirY, int dirZ, int steps) { + int depth = Math.max(1, steps); + IllegalArgumentException refused = null; + while (depth >= 1) { + try { + ConeWalk aimed = ConeWalk.aimed(origin, dirX, dirY, dirZ, halfAngleRadians, + depth * strideCells, strideCells); + if (aimed.totalLooks() <= maxCells) { + return aimed; + } + } catch (IllegalArgumentException tooLarge) { + refused = tooLarge; // too many looks to even count: the same answer, sooner + } + if (depth == 1) { + break; + } + depth = Math.max(1, depth / 2); } - return half; - } - - private static long volumeOf(int half) { - long side = 2L * half + 1L; - return side * side * side; + // A single shell that still will not fit means the aperture is wider than the ceiling can + // ever afford, which is a configuration nobody can survey with — and the operator has to + // be told which of the two numbers to change. + throw new IllegalArgumentException("a pointing of half-angle " + + String.format("%.3f", Math.toDegrees(halfAngleRadians)) + " degrees holds more" + + " than " + maxCells + " looks in its very first shell." + + " Narrow telescopeConeHalfAngleDegrees or raise telescopeScanMaxCells." + + (refused == null ? "" : " (" + refused.getMessage() + ")")); } } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/StellarMagnitude.java b/src/main/java/zmaster587/advancedRocketry/universe/StellarMagnitude.java new file mode 100644 index 000000000..cabf16281 --- /dev/null +++ b/src/main/java/zmaster587/advancedRocketry/universe/StellarMagnitude.java @@ -0,0 +1,185 @@ +package zmaster587.advancedRocketry.universe; + +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; + +/** + * How bright a star LOOKS from somewhere else — the photometry a telescope is bounded by. + * + *

    An instrument does not reach a distance; it reaches a BRIGHTNESS. Everything a survey can find + * is what stands above its limiting magnitude, and distance enters only through the inverse-square + * law that dims things. Stating an instrument's reach as a length is therefore stating a consequence + * as if it were a cause: the same telescope sees a blue giant eighty times farther than a red dwarf, + * and no single number of light years describes both.

    + * + *

    The unit is already spoken here. {@link Nebula#MAGNITUDES_PER_DENSITY_LIGHT_YEAR} turns a + * dust column into magnitudes of extinction and {@link UniverseRegistry#extinctionBetween} returns + * them, so dust and distance are two terms of ONE sum rather than two mechanics that have to be + * reconciled. That is the whole reason a magnitude limit is the right bound and a light-year horizon + * was the wrong one.

    + * + *

    Three quantities, in the order they are derived:

    + *
      + *
    1. Luminosity, from the star's own size and temperature — {@code L/L(sun) = R^2*(T/T(sun))^4}, + * the Stefan-Boltzmann law for a sphere.
    2. + *
    3. Absolute magnitude {@code M = M(sun) - 2.5*log10(L)} — how bright it would be at the + * standard ten parsecs.
    4. + *
    5. Apparent magnitude {@code m = M + 5*log10(d/10pc) + A} — how bright it is from here, + * through whatever dust {@code A} lies between.
    6. + *
    + * + *

    Magnitudes run BACKWARDS: smaller is brighter, and a difference of 5 is a factor of 100 in + * received flux. So "brighter than the limit" reads {@code m <= limit}, which is the one place this + * scale trips a reader who has not met it before.

    + */ +public final class StellarMagnitude { + + private StellarMagnitude() { + } + + /** + * The Sun's absolute visual magnitude — the zero point the whole scale is hung from. + * + *

    Measured, not chosen: 4.83 is the accepted value in the V band, and every absolute magnitude + * below is stated relative to it. Changing it does not rescale the sky, it moves the Sun.

    + */ + public static final double SOLAR_ABSOLUTE_MAGNITUDE = 4.83d; + + /** Light years in one parsec — 3.26156, the conversion the magnitude law's {@code 10 pc} needs. */ + public static final double LIGHT_YEARS_PER_PARSEC = 3.26156d; + + /** + * The temperature this layer calls the Sun's. + * + *

    {@link StellarBody#getTemperature()} is in units of a hundredth of Sol and not in + * kelvin, whatever its javadoc says — the stock table seats a sun-like star at 100 and a red dwarf + * at 40, and every consumer in the mod reads it that way. It is spelled out here because this + * class raises it to the FOURTH power, where a wrong unit is not a small error.

    + */ + public static final double SOLAR_TEMPERATURE_UNITS = 100d; + + /** + * How luminous a star of {@code radiusSuns} and {@code temperatureUnits} is, in Suns. + * + *

    {@code L = 4*pi*R^2*sigma*T^4} for both, divided: {@code L/L(sun) = (R/R(sun))^2*(T/T(sun))^4}. + * The fourth power is what makes the sky's brightness so unlike its population — a blue star is + * 0.13 % of the stars and outshines a red dwarf by nearly four orders.

    + */ + public static double luminositySuns(double radiusSuns, double temperatureUnits) { + double r = Math.max(0d, radiusSuns); + double t = Math.max(0d, temperatureUnits) / SOLAR_TEMPERATURE_UNITS; + return r * r * t * t * t * t; + } + + /** + * The same for a star object. + * + *

    An unstated temperature is read as Sol's, and that is a decision worth seeing. + * {@link StellarBody} leaves temperature at zero until something sets it, and zero raised to the + * fourth power is a star that emits nothing — so a pack that describes a star by its size alone + * would have written an invisible one, and it would have found out by pointing a telescope at + * empty sky. Zero here means UNSTATED, not cold, exactly as an unstated bulk means one Earth + * everywhere else in this layer. A star that really is dark says so by being a black hole.

    + */ + public static double luminositySuns(StellarBody star) { + if (star == null) { + return 0d; + } + // A black hole emits nothing a survey in the visible could catch. It is not "very faint" — + // it is off this scale entirely, and the caller's own "never detected" branch is the right one. + if (star.isBlackHole()) { + return 0d; + } + int temperature = star.getTemperature(); + return luminositySuns(star.getSize(), + temperature > 0 ? temperature : SOLAR_TEMPERATURE_UNITS); + } + + /** + * The absolute magnitude of a star of {@code luminositySuns} — how bright it would look at ten + * parsecs. Infinite for a star that emits nothing, which is the honest answer and never a number + * a comparison would accidentally accept. + */ + public static double absoluteMagnitude(double luminositySuns) { + if (!(luminositySuns > 0d)) { + return Double.POSITIVE_INFINITY; + } + return SOLAR_ABSOLUTE_MAGNITUDE - 2.5d * Math.log10(luminositySuns); + } + + /** + * How bright a star of absolute magnitude {@code absolute} looks from {@code distanceLightYears} + * away through {@code extinctionMagnitudes} of dust. + * + *

    The distance modulus {@code 5*log10(d/10pc)} is undefined at zero distance and enormous just + * above it, so a look from inside the star's own cell is answered with the absolute magnitude + * alone rather than with minus infinity: standing on top of something is not an observation, and + * a survey's own system is found by being there rather than by being seen.

    + */ + public static double apparentMagnitude(double absolute, double distanceLightYears, + double extinctionMagnitudes) { + if (Double.isInfinite(absolute)) { + return Double.POSITIVE_INFINITY; + } + double parsecs = Math.max(0d, distanceLightYears) / LIGHT_YEARS_PER_PARSEC; + double modulus = (parsecs <= 1e-9d) ? 0d : 5d * Math.log10(parsecs / 10d); + return absolute + modulus + Math.max(0d, extinctionMagnitudes); + } + + /** The same, straight from a star's own bulk — the form a detection stage calls. */ + public static double apparentMagnitudeOf(StellarBody star, double distanceLightYears, + double extinctionMagnitudes) { + return apparentMagnitude(absoluteMagnitude(luminositySuns(star)), distanceLightYears, + extinctionMagnitudes); + } + + /** + * How far a star of {@code luminositySuns} stays above {@code limitMagnitude} in CLEAR sky, in + * light years — the inverse of the distance modulus, and the number that TRUNCATES a survey. + * + *

    This is what replaces a configured horizon. An instrument's reach is the range of the + * brightest thing it could possibly see: past that nothing is detectable at any density, so the + * walk stops rather than being stopped. Dust only ever shortens it, so a reach computed with no + * extinction is an upper bound and a survey that walks it misses nothing.

    + * + *

    Zero for a star that emits nothing.

    + */ + public static double detectionRangeLightYears(double luminositySuns, double limitMagnitude) { + double absolute = absoluteMagnitude(luminositySuns); + if (Double.isInfinite(absolute)) { + return 0d; + } + double parsecs = Math.pow(10d, (limitMagnitude - absolute) / 5d + 1d); + if (Double.isInfinite(parsecs) || Double.isNaN(parsecs)) { + return Double.MAX_VALUE; + } + return Math.max(0d, parsecs * LIGHT_YEARS_PER_PARSEC); + } + + /** + * The reach of an instrument of {@code limitMagnitude} against the brightest of + * {@code archetypes} — the physical horizon of a survey aimed with it. + * + *

    Every archetype is asked and the widest wins, because a survey does not know what it is + * about to find. The brightest is not the hottest nor the largest but the one whose {@code R^2T^4} + * is greatest, which is why this is computed rather than read off the end of the table.

    + * + *

    A generator with no archetypes at all reaches nothing, and that is the honest answer: an + * empty universe has nothing to see, and a survey of it should be instantly complete rather than + * long and fruitless.

    + */ + public static double instrumentReachLightYears(Iterable archetypes, + double limitMagnitude) { + if (archetypes == null) { + return 0d; + } + double best = 0d; + for (GalaxyGenConfig.StarType type : archetypes) { + // The archetype's BRIGHTEST realisation: a star's size is drawn from a band, and the reach + // has to cover the brightest star the band can produce or the walk would stop short of + // something it can see. + double luminosity = luminositySuns(type.maxSize, type.temperature); + best = Math.max(best, detectionRangeLightYears(luminosity, limitMagnitude)); + } + return best; + } +} diff --git a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java index 530adddef..2073fb0f2 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java @@ -1,5 +1,8 @@ package zmaster587.advancedRocketry.universe; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.function.IntFunction; @@ -7,6 +10,7 @@ import zmaster587.advancedRocketry.api.ARConfiguration; import zmaster587.advancedRocketry.api.Constants; +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; import zmaster587.advancedRocketry.dimension.DimensionProperties; import zmaster587.advancedRocketry.item.ItemMemoryCrystal; import zmaster587.advancedRocketry.navigation.CrystalEntry; @@ -14,24 +18,53 @@ import zmaster587.advancedRocketry.space.GalacticCoord; /** - * Turns surveyed cells into addresses a ship can navigate by. + * Turns a pointing into addresses a ship can navigate by. * *

    This is the discovery instrument, so it asks the registry what is THERE rather than what is * already known: an instrument that only reported what the player had already found could never find * anything.

    * - *

    What a cell yields is its system's bodies, one address each, at the coarsest detail an - * observation can carry. That grade is not a formality — it is what the navigation console reads to - * decide which of a body's fields it may show, and at telescope grade that is already the whole - * global set: name, mass, stellar class, rings, sky colour, topology, atmosphere and its density, - * temperature, water. A cell whose system has no resolvable content still yields its bare - * coordinate, so the address is learned even when nothing can yet be said about it.

    + *

    Two stages, because they are two different questions and only one of them is expensive.

    + *
      + *
    1. Detection ({@link #detect}) — is anything in this direction, and is it bright enough + * to register? An anchor lookup and a magnitude, both O(1), with no bodies built and no + * retinue derived. This is what a survey spends its looks on.
    2. + *
    3. Characterisation ({@link #characterise}) — what IS it? The system's bodies, one + * address each. Paid only where the first stage found something.
    4. + *
    + * + *

    They used to be one call, so the cheap question could never be asked without paying for the + * expensive one. {@link InfoTier} already distinguished the two grades of knowledge; what was missing + * was an instrument that could hold one without the other.

    + * + *

    What a look sees is bounded by BRIGHTNESS, never by distance. A star registers when its + * apparent magnitude from the observatory — its own luminosity, dimmed by distance and by whatever + * dust lies between — is above the aperture's limit. So the same instrument reaches a blue giant + * eighty times farther than a red dwarf, and a starless world it never reaches at all: a rogue + * planet emits nothing, and finding one is a thing you do by going there.

    + * + *

    What a cell yields once characterised is its system's bodies, one address each, at the + * coarsest detail an observation can carry. That grade is not a formality — it is what the navigation + * console reads to decide which of a body's fields it may show, and at telescope grade that is + * already the whole global set: name, mass, stellar class, rings, sky colour, topology, atmosphere + * and its density, temperature, water.

    */ public final class TelescopeScan { private TelescopeScan() { } + /** + * The most seats one look will enumerate inside its own territory before it goes back to + * sampling — see {@link IGalaxyGenerator#anchorsInTerritory}. + * + *

    Sized by what a UNIFORMLY divided field can hold, not by a feel for a good batch: a lattice + * divided {@code k} ways per axis puts {@code k³} seats in a territory, and 64 covers every + * division up to four. Past that the divider is a star cluster, where a survey samples rather + * than counts and always has.

    + */ + public static final int MAX_SEATS_PER_LOOK = 64; + /** How production names a body: by its dimension, the way every other GUI does. */ public static IntFunction dimensionNames() { return dimId -> { @@ -42,25 +75,177 @@ public static IntFunction dimensionNames() { } /** - * Resolve the next {@code count} cells of {@code scan} onto {@code crystal}. + * One point the instrument registered: where it is, and how it looked from where the instrument + * stands. + * + *

    The magnitude and the dust are carried rather than recomputed because the second stage needs + * them to decide how much it can make out — and because a detection is a fact about a LOOK, not + * about a system: the same star is a different detection from somewhere else.

    + */ + public static final class Detection { + + private final GalacticCoord anchor; + private final double apparentMagnitude; + private final double distanceLightYears; + private final double extinctionMagnitudes; + + public Detection(GalacticCoord anchor, double apparentMagnitude, double distanceLightYears, + double extinctionMagnitudes) { + this.anchor = anchor; + this.apparentMagnitude = apparentMagnitude; + this.distanceLightYears = distanceLightYears; + this.extinctionMagnitudes = extinctionMagnitudes; + } + + /** The anchor cell of the system that was registered. */ + public GalacticCoord anchor() { + return anchor; + } + + /** How bright it looked from the instrument. Magnitudes: smaller is brighter. */ + public double apparentMagnitude() { + return apparentMagnitude; + } + + /** How far away it stands, in light years. */ + public double distanceLightYears() { + return distanceLightYears; + } + + /** How much dust lies between, in magnitudes of extinction. */ + public double extinctionMagnitudes() { + return extinctionMagnitudes; + } + + @Override + public String toString() { + return "Detection[" + anchor.cellKey() + ", m=" + String.format("%.2f", apparentMagnitude) + + ", " + String.format("%.1f", distanceLightYears) + " ly]"; + } + } + + /** + * STAGE ONE. Everything in {@code look}'s star territory that is bright enough to register from + * {@code observer}. + * + *

    The territory and not the point. A survey strides by the star territory, so a look + * that resolved only the point it landed on would report one seat in however many the generator + * divides that cube into — a fraction of the sky, presented as the sky. Asking for the + * territory's anchors makes the answer independent of how finely the field happens to be + * divided, which is the property a survey needs and a stride cannot give it.

    + * + *

    A null observer means the look is free of geometry: no distance, no dust, and + * everything present registers. That is what a caller with no position can honestly claim, and + * what every look was before an instrument had somewhere to stand.

    + */ + public static List detect(UniverseRegistry registry, GalacticCoord look, + GalacticCoord observer, double limitMagnitude) { + if (registry == null || look == null) { + return Collections.emptyList(); + } + List anchors = registry.anchorsInTerritory(look, MAX_SEATS_PER_LOOK); + if (anchors.isEmpty()) { + return Collections.emptyList(); + } + List hits = new ArrayList<>(anchors.size()); + for (GalacticCoord anchor : anchors) { + if (observer == null) { + hits.add(new Detection(anchor, Double.NEGATIVE_INFINITY, 0d, 0d)); + continue; + } + // The STATIC-frame separation, which is the right one here and not an approximation: an + // anchor's frame really does sit at sector*CELL forever, and a survey looks at anchors. + double cells = observer.cellCentre().staticFrameDistanceTo(anchor.cellCentre()) + / (double) GalacticCoord.CELL; + double lightYears = UniverseScale.lightYearsForCells(cells); + StellarBody star = registry.starAt(anchor).orElse(null); + // CLEAR SKY FIRST, and this ordering is not a micro-optimisation — it is the difference + // between a survey that runs and one that does not. Measuring the dust on a sight line + // means integrating a cloud field along the whole of it, which is by far the dearest + // thing on this path, and extinction can only ever make a star DIMMER. So anything + // already too faint in a clear sky is rejected without asking about the dust, and a + // full pointing pays for the integral a dozen times instead of half a million. + double clearSky = StellarMagnitude.apparentMagnitudeOf(star, lightYears, 0d); + if (clearSky > limitMagnitude) { + continue; + } + double extinction = registry.extinctionBetween(observer, anchor); + double magnitude = clearSky + extinction; + if (magnitude <= limitMagnitude) { + hits.add(new Detection(anchor, magnitude, lightYears, extinction)); + } + } + return hits; + } + + /** + * STAGE TWO. Write down what {@code hit} turns out to be. + * + *

    A look is a touch. Everything here hands the operator something durable — an address + * he can fly to, a body he can name — out of a derivation that a later seed, config or generator + * edit would answer differently. Pinning first freezes the system into the save before a word of + * it is written down, so what the crystal holds and what the sky holds cannot come apart. The + * unit is the whole SYSTEM and not the bodies enumerated, because a system is what a pin can key. + * Idempotent and free for anything already authored or pinned.

    + * + *

    An unresolvable look still yields an address. Whether the dust was too thick or the + * operator has the instrument set to record positions only, the bare coordinate is written: the + * operator learns that something is there and has to go and see what. That is the whole + * mechanic — a reason to FLY somewhere rather than survey it from home — and it is why + * concealment costs detail and never the look itself.

    + * + * @param wholeSystem whether to enumerate the system's bodies, or record the address alone. The + * operator's own choice: a full characterisation is the instrument's dear + * setting and fills a crystal far faster + * @return how many entries the memory gained or refreshed + */ + public static int characterise(UniverseRegistry registry, Detection hit, CrystalMemory memory, + long observedTick, IntFunction nameOf, + boolean wholeSystem) { + if (registry == null || hit == null || memory == null) { + return 0; + } + GalacticCoord anchor = hit.anchor(); + registry.pinSystem(anchor); + int written = 0; + boolean namedSomething = false; + if (wholeSystem && !isObscuredAt(hit.extinctionMagnitudes())) { + for (SystemBody body : registry.systemBodiesAt(anchor)) { + namedSomething = true; + if (memory.record(entryFor(body, observedTick, nameOf))) { + written++; + } + } + } + if (!namedSomething) { + PlanetarySystem system = registry.systemForCoord(anchor).orElse(null); + if (memory.record(entryForSystem(anchor, system, observedTick))) { + written++; + } + } + return written; + } + + /** + * Resolve the next {@code count} looks of {@code scan} onto {@code crystal}. * * @return how many entries the crystal gained or refreshed */ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int from, int count, ItemStack crystal, long observedTick, IntFunction nameOf) { - return resolveBatch(registry, scan, from, count, crystal, observedTick, nameOf, null); + return resolveBatch(registry, scan, from, count, crystal, observedTick, nameOf, null, true); } - /** The same, resolved from a stated observer, so a cloud in the way costs the look its detail. */ + /** The same, resolved from a stated observer, so distance and dust decide what registers. */ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int from, int count, ItemStack crystal, long observedTick, IntFunction nameOf, - GalacticCoord observer) { + GalacticCoord observer, boolean wholeSystem) { if (!ItemMemoryCrystal.isCrystal(crystal)) { return 0; } CrystalMemory memory = ItemMemoryCrystal.memoryOf(crystal); int written = resolveBatch(registry, scan, from, count, memory, observedTick, nameOf, - observer); + observer, wholeSystem); if (written > 0) { ItemMemoryCrystal.writeMemory(crystal, memory); } @@ -70,29 +255,54 @@ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int f /** The same, onto an already-opened memory. This is where the discovery actually happens. */ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int from, int count, CrystalMemory memory, long observedTick, IntFunction nameOf) { - return resolveBatch(registry, scan, from, count, memory, observedTick, nameOf, null); + return resolveBatch(registry, scan, from, count, memory, observedTick, nameOf, null, true); } /** - * The same, resolved from a stated OBSERVER — the form that can see what is in the way. - * - *

    A null observer means "nothing is between us and it", which is what a caller with no - * position can honestly claim, and what every look was before clouds could obscure one.

    + * The same, resolved from a stated OBSERVER — the form that can see how far away and how dim + * something is. */ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int from, int count, CrystalMemory memory, long observedTick, IntFunction nameOf, - GalacticCoord observer) { + GalacticCoord observer, boolean wholeSystem) { if (registry == null || scan == null || memory == null) { return 0; } + double limit = limitMagnitude(); int written = 0; for (int index = from; index < from + count && index < scan.totalCells(); index++) { - written += resolveCell(registry, scan.cellAt(index), memory, observedTick, nameOf, - observer); + written += resolveLook(registry, scan.cellAt(index), memory, observedTick, nameOf, + observer, limit, wholeSystem); + } + return written; + } + + /** + * ONE look, both stages: what is in this direction's territory, and what those things are. + * + *

    The question a look asks is which systems this territory holds, never "is a star + * seated exactly at this point". A system is a neighbourhood: its star holds the anchor cell and + * every planet holds one of its own, so a cell that is a system's planet — or simply the space + * between its bodies — is a cell that resolves to that system. Asking whether the cell IS the + * seat means a survey discovers a system only by landing on its star's own address, which for a + * lattice thousands of cells wide is a thing that never happens.

    + */ + public static int resolveLook(UniverseRegistry registry, GalacticCoord look, CrystalMemory memory, + long observedTick, IntFunction nameOf, + GalacticCoord observer, double limitMagnitude, + boolean wholeSystem) { + int written = 0; + for (Detection hit : detect(registry, look, observer, limitMagnitude)) { + written += characterise(registry, hit, memory, observedTick, nameOf, wholeSystem); } return written; } + /** The aperture the running game is configured with. Magnitudes: larger is fainter. */ + public static double limitMagnitude() { + return ARConfiguration.getCurrentConfig().telescopeLimitingMagnitude; + } + /** * Whether a look from {@code observer} to {@code target} is OBSCURED — a cloud between them thick * enough that a survey can no longer make out what is there, only that something is. @@ -100,87 +310,24 @@ public static int resolveBatch(UniverseRegistry registry, RegionScan scan, int f *

    The threshold is read in magnitudes of extinction, the unit the sky is measured in, and its * shipped default is the astronomical boundary at which faint objects behind a cloud disappear. * Zero or less turns the whole mechanic off, which is what "disable the flag" has to mean.

    + * + *

    It COMPOSES with the aperture rather than duplicating it, on the same currency: the same + * dust is added to the star's apparent magnitude, so a thick enough cloud takes the system below + * the limit and it is never detected at all. Between the two lies the interesting band — bright + * enough to see, dim enough that nothing about it can be made out.

    */ public static boolean isObscured(UniverseRegistry registry, GalacticCoord observer, GalacticCoord target) { if (registry == null || observer == null || target == null) { return false; } - double threshold = ARConfiguration.getCurrentConfig().telescopeObscuredAtMagnitudes; - if (!(threshold > 0d)) { - return false; - } - return registry.extinctionBetween(observer, target) >= threshold; + return isObscuredAt(registry.extinctionBetween(observer, target)); } - /** - * Resolve ONE cell: every body of the system that OWNS it, or the bare coordinate when that - * system has no content the registry can name. Void space yields nothing, which is the point of - * asking at all — an empty sky must not manufacture an address. - * - *

    The question is which system owns this cell, never "is a star seated exactly here". - * A system is a neighbourhood: its star holds the anchor cell and every planet holds one of its - * own, so a cell that is a system's planet — or simply the space between its bodies — is a cell - * that resolves to that system. Asking whether the cell IS the seat means a survey discovers a - * system only by landing on its star's own address, which for a lattice a few thousand cells wide - * is a thing that never happens. Resolving through the owner is also what lets an observatory - * standing on a planet report the system it is standing in.

    - */ - public static int resolveCell(UniverseRegistry registry, GalacticCoord cell, CrystalMemory memory, - long observedTick, IntFunction nameOf) { - return resolveCell(registry, cell, memory, observedTick, nameOf, null); - } - - /** - * The same, from a stated OBSERVER, so a cloud in the way can cost the look its detail. - * - *

    An obscured look still yields an address. It falls back to the same bare coordinate a - * system with nothing enumerable already produced: the operator learns that something is there - * and has to go and see what. That is the whole mechanic — a reason to FLY somewhere rather than - * survey it from home — and it is why concealment costs detail and never the look itself. A - * survey that quietly returned nothing would be indistinguishable from an empty sky, which is - * the exact defect this instrument was carrying until it was fixed.

    - */ - public static int resolveCell(UniverseRegistry registry, GalacticCoord cell, CrystalMemory memory, - long observedTick, IntFunction nameOf, - GalacticCoord observer) { - if (registry == null || cell == null || memory == null) { - return 0; - } - Optional anchor = registry.anchorForCell(cell); - if (!anchor.isPresent()) { - return 0; - } - // A LOOK IS A TOUCH. Everything below hands the operator something durable — an address he can - // fly to, a body he can name — out of a derivation that a later seed, config or generator edit - // would answer differently. Pinning first freezes the system into the save before a word of it - // is written down, so what the crystal holds and what the sky holds cannot come apart. - // - // The unit is the whole SYSTEM and not the bodies enumerated, because a system is what a pin - // can key: an obscured look still yields the address and the primary kind, and those are the - // system's identity. Freezing bodies the operator has not resolved yet is the conservative - // direction — they are what he will find when he gets there. - // - // Idempotent and free for anything already authored or pinned, so a re-scan of known sky and - // the many member cells of one system cost one pin between them. - registry.pinSystem(anchor.get()); - int written = 0; - boolean namedSomething = false; - if (!isObscured(registry, observer, anchor.get())) { - for (SystemBody body : registry.systemBodiesAt(anchor.get())) { - namedSomething = true; - if (memory.record(entryFor(body, observedTick, nameOf))) { - written++; - } - } - } - if (!namedSomething) { - PlanetarySystem system = registry.systemForCoord(anchor.get()).orElse(null); - if (memory.record(entryForSystem(anchor.get(), system, observedTick))) { - written++; - } - } - return written; + /** The same decision against an extinction already measured — what a detection carries. */ + public static boolean isObscuredAt(double extinctionMagnitudes) { + double threshold = ARConfiguration.getCurrentConfig().telescopeObscuredAtMagnitudes; + return threshold > 0d && extinctionMagnitudes >= threshold; } /** One body's address, at the coarsest grade, dated by when it was seen. */ @@ -194,8 +341,8 @@ public static CrystalEntry entryFor(SystemBody body, long observedTick, IntFunct } /** - * A system with nothing the registry can enumerate: the address alone, so a pilot can still aim - * at the light and go look. It names no body, because none has been resolved. + * A system nothing has been resolved of: the address alone, so a pilot can still aim at the light + * and go look. It names no body, because none has been resolved. */ public static CrystalEntry entryForSystem(GalacticCoord coord, PlanetarySystem system, long observedTick) { // The system's own name and its own PRIMARY KIND: a starless system recorded as a STAR would @@ -204,4 +351,25 @@ public static CrystalEntry entryForSystem(GalacticCoord coord, PlanetarySystem s SystemBodyKind kind = system == null ? SystemBodyKind.STAR : system.primaryKind(); return new CrystalEntry(coord.cellCentre(), name, kind, InfoTier.TELESCOPE, observedTick); } + + /** + * The bodies of the system owning {@code cell}, written down without any photometry — the form + * an instrument standing INSIDE a system uses to report what it is standing in. + * + *

    Kept as its own entry point rather than folded into a look, because it answers a different + * question: not "what can I see from here" but "what is here". Nothing about brightness applies + * to a system you are inside.

    + */ + public static int resolveCell(UniverseRegistry registry, GalacticCoord cell, CrystalMemory memory, + long observedTick, IntFunction nameOf) { + if (registry == null || cell == null || memory == null) { + return 0; + } + Optional anchor = registry.anchorForCell(cell); + if (!anchor.isPresent()) { + return 0; + } + return characterise(registry, new Detection(anchor.get(), Double.NEGATIVE_INFINITY, 0d, 0d), + memory, observedTick, nameOf, true); + } } diff --git a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java index 2c76c57e4..35552efc3 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/UniverseRegistry.java @@ -358,6 +358,26 @@ private static String superKey(GalacticCoord cell, int spacing) { } /** The stored (registered) system's star-id at this cell, or empty. Ignores the procedural generator. */ + /** + * Every anchor seated in the star TERRITORY {@code cell} falls in — what one look of a survey + * owes the direction it is pointed in (see {@link IGalaxyGenerator#anchorsInTerritory}). + * + *

    An authored or pinned anchor still wins over the whole territory, exactly as it does in + * {@link #anchorForCell}: a pack that placed a system there placed THE system there, and a + * procedural seat in the same cube would be a second answer to a question that has one.

    + */ + public List anchorsInTerritory(GalacticCoord cell, int limit) { + GalacticCoord c = cell.cellCentre(); + if (byCell.containsKey(c.cellKey())) { + return Collections.singletonList(c); + } + GalacticCoord stored = storedAnchorNear(c); + if (stored != null) { + return Collections.singletonList(stored); + } + return generator.anchorsInTerritory(worldSeed, c, limit); + } + public OptionalInt starIdForCoord(GalacticCoord coord) { Integer id = byCell.get(coord.cellCentre().cellKey()); return id == null ? OptionalInt.empty() : OptionalInt.of(id); diff --git a/src/main/resources/assets/advancedrocketry/lang/en_US.lang b/src/main/resources/assets/advancedrocketry/lang/en_US.lang index 1fa4acca4..a7972f2b3 100644 --- a/src/main/resources/assets/advancedrocketry/lang/en_US.lang +++ b/src/main/resources/assets/advancedrocketry/lang/en_US.lang @@ -423,8 +423,8 @@ msg.observetory.scan.distance=Distance (stars): msg.observetory.scan.distance.tooltip=How far out to look, counted in neighbouring stars. Farther is a longer observation, and the instrument has a horizon. msg.observetory.scan.lightyears= (%.1f ly) msg.observetory.scan.region=Observe -msg.observetory.scan.region.tooltip=Look at the chosen region and write every system it resolves onto the crystal -msg.observetory.scan.looking=Surveyed cells: +msg.observetory.scan.region.tooltip=Point the instrument along the chosen direction and write every system bright enough to register onto the crystal +msg.observetory.scan.looking=Looks taken: msg.observetory.scan.found=Addresses written: msg.observetory.scan.obscured=Dust in the way - coordinates only: msg.observetory.scan.idle=Idle @@ -433,6 +433,9 @@ msg.observetory.scan.abort.tooltip=Stop the survey. Everything already resolved msg.observetory.scan.mode.active=Deep msg.observetory.scan.mode.passive=Local msg.observetory.scan.mode.tooltip=Local watches the neighbourhood and has its data ready; deep looks at a chosen distant region. One at a time - an instrument staring into deep space cannot see what is close. +msg.observetory.scan.detail.full=Full +msg.observetory.scan.detail.coords=Positions +msg.observetory.scan.detail.tooltip=Full names every body of every system the survey registers; Positions writes down only where they are. A deep pointing on Full fills a crystal many times faster - and a position is still somewhere you can fly to and look. msg.observetory.scan.keepcrystal=Keep a crystal in the machine: a broken observatory loses what it holds. msg.observetory.text.asteroids=Asteroids msg.observetory.text.composition=Composition diff --git a/src/test/java/zmaster587/advancedRocketry/test/client/MachineGuiClientGroupE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/client/MachineGuiClientGroupE2ETest.java index 4b9a113dc..229955211 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/client/MachineGuiClientGroupE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/client/MachineGuiClientGroupE2ETest.java @@ -506,9 +506,8 @@ public void theOperatorAimsTheTelescopeAndObservesWithNothingButClicks() throws // is what this drives. exec("artest config set planetsMustBeDiscovered false"); exec("artest config set telescopeScanBaseTicks 0"); - exec("artest config set telescopeScanTicksPerLightYear 1"); - exec("artest config set telescopeScanHalfWidthSteps 1"); - exec("artest config set telescopeScanRangeLightYears 100"); + exec("artest config set telescopeLimitingMagnitude 30"); + exec("artest config set telescopeConeHalfAngleDegrees 20"); String crystal = exec("artest telescope crystal " + where); scenario().requireArranged("could not put a crystal in the observatory: " + crystal, crystal.contains("\"ok\":true")); diff --git a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java index 8a44e63f9..e801a1269 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/integration/SystemContentTest.java @@ -159,12 +159,17 @@ public void oneOrbitalDistanceMeansOneDistanceInBothFamilies() { // insists on one is testing the coin. // It must be a seat with a STAR: the comparison is between one authored planet's orbit and one // procedural planet's, and a starless system has no orbits at all to compare with. + // Asked what each TERRITORY holds, never what its corner point resolves to: the lattice is + // divided uniformly, so a point probe samples one seat in k-cubed and a sweep built on it + // reads a populated field as an almost empty one. Optional seat = Optional.empty(); for (long i = 1; i <= 16 && !seat.isPresent(); i++) { - Optional candidate = gen.anchorAt(0xBEEFL, - GalacticCoord.ofSectorLocal(i * spacing, spacing, spacing, 0L, 0L, 0L)); - if (candidate.isPresent() && gen.systemAt(0xBEEFL, candidate.get()).get().star().isPresent()) { - seat = candidate; + for (GalacticCoord candidate : gen.anchorsInTerritory(0xBEEFL, + GalacticCoord.ofSectorLocal(i * spacing, spacing, spacing, 0L, 0L, 0L), 64)) { + if (gen.systemAt(0xBEEFL, candidate).get().star().isPresent()) { + seat = Optional.of(candidate); + break; + } } } assertTrue("the fixture needs an occupied super-cell with a star in it", seat.isPresent()); diff --git a/src/test/java/zmaster587/advancedRocketry/test/server/TelescopeRegionScanE2ETest.java b/src/test/java/zmaster587/advancedRocketry/test/server/TelescopeRegionScanE2ETest.java index 48e914e2a..7c281987e 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/server/TelescopeRegionScanE2ETest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/server/TelescopeRegionScanE2ETest.java @@ -36,16 +36,18 @@ private static String join(java.util.List response) { * the default game, where what the instrument reaches is resolved outright; on is the research * mode, where the sweep is paced and the time curve is the mechanic. */ - private void surveySetup(boolean research, int cellsPerStep, double ticksPerLightYear) + private void surveySetup(boolean research, int cellsPerStep, int ticksPerStep) throws Exception { exec("artest config set planetsMustBeDiscovered " + research); - exec("artest config set telescopeScanBaseTicks 0"); - exec("artest config set telescopeScanTicksPerLightYear " + ticksPerLightYear); - exec("artest config set telescopeScanRangeLightYears 100"); - exec("artest config set telescopeScanHalfWidthSteps 1"); + exec("artest config set telescopeScanBaseTicks " + ticksPerStep); + // An aperture that sees essentially anything, so what a fixture finds is decided by where it + // put the fixture and never by how bright the sky happened to draw it. A survey's photometry + // is pinned in the unit tier, where a star's luminosity can be stated. + exec("artest config set telescopeLimitingMagnitude 30"); + exec("artest config set telescopeConeHalfAngleDegrees 20"); exec("artest config set telescopeScanMaxCells 1000"); exec("artest config set telescopeScanCellsPerStep " + cellsPerStep); - exec("artest config set telescopePassiveRadiusCells 1"); + exec("artest config set telescopePassiveRadiusSteps 1"); } /** How far apart, in cells, the looks of a directed survey stand in THIS server's universe. */ @@ -144,7 +146,7 @@ private String awaitSurveyComplete(int x) throws Exception { @Test public void withoutResearchWhatTheInstrumentReachesIsResolvedOutright() throws Exception { final int x = 4300; - surveySetup(false, 2, 40); + surveySetup(false, 2, 120); String[] home = observatoryWithCrystal(x); systemNearTheLookAt(x, home, 4); @@ -164,8 +166,9 @@ public void withResearchTheSurveySweepsCellByCell() throws Exception { // One cell a step — the claim under test — and a step short enough that 27 of them fit in // the poll budget: at 20 ticks per sector of distance a single cell took 4 s, so the whole // region wanted 108 s against a 10 s budget and the sweep was blamed for the arithmetic. - // Priced per LIGHT YEAR now, and four steps out is ~17 of them, so the rate is a fraction. - surveySetup(true, 1, 0.2); + // Priced flat per STEP now: a pointing's cost in time is carried by how many steps it + // needs, because a deeper one already holds proportionally more looks. + surveySetup(true, 1, 3); String[] home = observatoryWithCrystal(x); String started = exec("artest telescope scan " + where(x) + " 1 0 0 4"); @@ -194,7 +197,7 @@ public void withResearchTheSurveySweepsCellByCell() throws Exception { @Test public void stoppingASurveyIsFreeAndKeepsWhatWasAlreadyLearned() throws Exception { final int x = 4380; - surveySetup(true, 1, 60); + surveySetup(true, 1, 180); String[] home = observatoryWithCrystal(x); systemNearTheLookAt(x, home, 3); @@ -215,7 +218,7 @@ public void stoppingASurveyIsFreeAndKeepsWhatWasAlreadyLearned() throws Exceptio @Test public void aimingAgainMovesTheRegionWithoutLosingWhatWasLearned() throws Exception { final int x = 4420; - surveySetup(true, 1, 60); + surveySetup(true, 1, 180); String[] home = observatoryWithCrystal(x); String first = exec("artest telescope scan " + where(x) + " 1 0 0 3"); @@ -224,8 +227,11 @@ public void aimingAgainMovesTheRegionWithoutLosingWhatWasLearned() throws Except String second = exec("artest telescope scan " + where(x) + " 0 0 1 5"); assertTrue("re-aiming mid-survey must be allowed: " + second, second.contains("\"ok\":true")); - assertNotEquals("re-aiming must actually move the region", - text(first, "min"), text(second, "min")); + // The DIRECTION and not the corners. A pointing's bounding box is its apex plus its reach + // on every axis, so re-aiming the same instrument leaves min/max exactly where they were — + // the aim is where it is looking, which is a vector. + assertNotEquals("re-aiming must actually move the pointing", + text(first, "dir"), text(second, "dir")); assertTrue("and must keep every address already written: " + second, field(second, "addresses") >= learned); } @@ -233,7 +239,7 @@ public void aimingAgainMovesTheRegionWithoutLosingWhatWasLearned() throws Except @Test public void theLocalRadarSurveysTheObservatorysOwnNeighbourhood() throws Exception { final int x = 4460; - surveySetup(false, 4, 40); + surveySetup(false, 4, 120); String[] home = observatoryWithCrystal(x); String passive = exec("artest telescope passive " + where(x)); @@ -249,8 +255,9 @@ public void theLocalRadarSurveysTheObservatorysOwnNeighbourhood() throws Excepti long hi = Long.parseLong(maxKey.split("_")[0]); assertTrue("the radar must look around home (" + homeX + "), not at " + minKey + ".." + maxKey, lo <= homeX && homeX <= hi); - assertEquals("and it must walk CELLS: close to home the next cell is a different destination", - 1L, field(passive, "stride")); + assertEquals("and it must walk TERRITORIES: one look already yields every body of the system " + + "that owns it, so a neighbourhood is measured in NEIGHBOURS", + field(passive, "stepCells"), field(passive, "stride")); // An observatory stands on a PLANET, never on its own star. Under the gate this test was // written against, the cell it is standing in reported empty and the machine could not name @@ -265,7 +272,7 @@ public void aSurveyInFlightSurvivesItsChunkBeingUnloaded() throws Exception { final int x = 4540; // Research on, one cell a step, a step long enough that the sweep is certainly mid-region // when the chunk goes away. - surveySetup(true, 1, 10); + surveySetup(true, 1, 30); observatoryWithCrystal(x); String started = exec("artest telescope scan " + where(x) + " 1 0 0 3"); @@ -293,7 +300,7 @@ public void aSurveyInFlightSurvivesItsChunkBeingUnloaded() throws Exception { @Test public void anUnfedInstrumentStallsInsteadOfSurveyingForFree() throws Exception { final int x = 4580; - surveySetup(true, 1, 1); + surveySetup(true, 1, 3); // A price no bare observatory can pay: it has no data buses, so it has no distance data. exec("artest config set telescopeSurveyDataPerStep 50"); try { @@ -315,7 +322,7 @@ public void anUnfedInstrumentStallsInsteadOfSurveyingForFree() throws Exception @Test public void whatTheTelescopeWroteIsWhatAShipCanBeAimedBy() throws Exception { final int x = 4620; - surveySetup(false, 4, 1); + surveySetup(false, 4, 3); String[] home = observatoryWithCrystal(x); systemNearTheLookAt(x, home, 3); @@ -344,7 +351,7 @@ public void theHorizonIsALengthAnInstrumentCouldActuallyHave() throws Exception // The half of the defect that no amount of resolving would have fixed: the reach was stated // in cells, so 24 of them was 0.16 AU — a fifth of the way to Mercury — and every aim inside // the horizon stayed inside the solar system. A horizon is a LENGTH. - surveySetup(false, 4, 1); + surveySetup(false, 4, 3); observatoryWithCrystal(x); String idle = exec("artest telescope info " + where(x)); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java index 0da7751ed..0e93fd07e 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/ClusteredGalaxyGeneratorTest.java @@ -545,27 +545,36 @@ public void tinySpacingDegeneratesIntoALoneStar() { } @Test - public void anchorAtAttributesEveryCellOfAnOccupiedSuperCell() { + public void everyBodyOfASystemAttributesBackToThatSystemsAnchor() { + // MEMBER-CELL ATTRIBUTION, which is what every address in the game rests on: a body is + // reached, described and landed on through the system that owns its cell, so + // "which system owns this cell" must have exactly one answer and it must be the right one. + // + // It used to be stated as "every cell of a SUPER-CELL attributes to that super-cell's + // anchor", and that sentence stopped being true when the lattice began to be divided + // uniformly: a territory holds up to k-cubed seats, so two cells of one super-cell honestly + // belong to two different systems. What did NOT change — and what the old wording was + // standing in for — is that a system's own bodies all attribute back to it. That is the + // property the console, the descent trigger and the sky all read, and unlike the old one it + // is stated against the unit that actually owns a neighbourhood. GalaxyGenConfig config = cfg(0.9d, SPACING); ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); long s = config.minSpacing; boolean checkedAny = false; for (long sup = -2; sup <= 2; sup++) { - Optional anchor = gen.anchorAt(SEED, cell(sup * s, 0, 0)); - if (!anchor.isPresent()) { - continue; - } - checkedAny = true; - // Every cell of the super-cell attributes to the SAME anchor (corners included). - for (long dx : new long[] {0, s - 1}) { - for (long dy : new long[] {0, s - 1}) { - GalacticCoord member = cell(sup * s + dx, dy, 0); - assertEquals("member " + member + " must attribute to the super-cell's anchor", - anchor, gen.anchorAt(SEED, member)); + for (GalacticCoord anchor : gen.anchorsInTerritory(SEED, cell(sup * s, 0, 0), 64)) { + checkedAny = true; + // The anchor itself point-resolves to the system, and to itself. + assertTrue(gen.systemAt(SEED, anchor).isPresent()); + assertEquals("an anchor must attribute to itself", + Optional.of(anchor), gen.anchorAt(SEED, anchor)); + + for (SystemBody body : gen.bodiesFor(SEED, anchor)) { + assertEquals("body " + body.name().cellKey() + " of the system at " + + anchor.cellKey() + " must attribute back to it", + Optional.of(anchor), gen.anchorAt(SEED, body.name())); } } - // The anchor itself point-resolves to the system. - assertTrue(gen.systemAt(SEED, anchor.get()).isPresent()); } assertTrue(checkedAny); } @@ -798,23 +807,26 @@ public void theFieldStandsAsFarApartAsTheConstantSaysItDoes() { // every territory a star left empty, so counting systems measures occupancy 1.0 // and says nothing about how far apart the STARS stand — which is the quantity // MEAN_STAR_SEPARATION_LY is about. (Measured here first: 4913 of 4913.) - // Through the ANCHOR: systemAt answers on the seat cell alone, and a territory's - // corner is not its seat. - Optional anchor = gen.anchorAt(SEED, + // What the whole TERRITORY holds. Two things are wrong with resolving its + // corner point instead: systemAt answers on the seat cell alone and a corner is + // not a seat, AND the lattice is divided uniformly, so one point is one seat in + // k-cubed — a sweep built on it measured a full field as 1.3 % occupied. + for (GalacticCoord anchor : gen.anchorsInTerritory(SEED, cell((long) i * config.minSpacing, (long) j * config.minSpacing, - (long) k * config.minSpacing)); - if (!anchor.isPresent()) { - continue; - } - Optional here = gen.systemAt(SEED, anchor.get()); - if (here.isPresent() && here.get().star().isPresent()) { - seated++; + (long) k * config.minSpacing), 64)) { + Optional here = gen.systemAt(SEED, anchor); + if (here.isPresent() && here.get().star().isPresent()) { + seated++; + } } } } } assertTrue("arrangement: the sweep must find a populated star field", seated > territories / 10); + // Stars PER TERRITORY, which is what the separation formula wants and is no longer the same + // thing as "the fraction of territories that hold one": a territory now holds up to k-cubed + // seats, so the two numbers come apart the moment more than one of them is taken. double occupancy = seated / (double) territories; double separation = UniverseScale.meanSeparationLy(config.minSpacing, occupancy); double claimed = UniverseScale.MEAN_STAR_SEPARATION_LY; @@ -932,9 +944,15 @@ public void aGeneratorDerivesItsBodiesThroughTheDerivationItWasGiven() { GalacticCoord anchor = null; for (int i = 0; i < 64 && anchor == null; i++) { GalacticCoord probe = GalacticCoord.ofSectorLocal((long) i * config.minSpacing, 0, 0, 0, 0, 0); - java.util.Optional found = stock.anchorAt(SEED, probe); - if (found.isPresent() && !stock.bodiesFor(SEED, found.get()).isEmpty()) { - anchor = found.get(); + for (GalacticCoord found : stock.anchorsInTerritory(SEED, probe, 64)) { + // A system with a RETINUE. A territory's seats include unbound worlds, which hold one + // body and no orbit law to move — so a probe that took the first seat it found would + // compare two derivations on a system neither of them can express differently. + if (stock.systemAt(SEED, found).flatMap(sys -> sys.star()).isPresent() + && stock.bodiesFor(SEED, found).size() > 1) { + anchor = found; + break; + } } } assertTrue("arrangement: a system with bodies must be found near the origin", anchor != null); diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java index 841c728d6..6691d57cd 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/NebulaConcealmentTest.java @@ -92,8 +92,11 @@ private static UniverseRegistry oneSystem() { } private static int look(UniverseRegistry registry, CrystalMemory crystal) { - return TelescopeScan.resolveCell(registry, TARGET, crystal, 7_000L, - dimId -> "Body-" + dimId, HOME); + // An aperture nothing in this fixture can fall below, because what is under test is the + // DUST and not the brightness: a limit that also gated the look would make "the dusty case + // named nothing" true for two reasons and pin neither. + return TelescopeScan.resolveLook(registry, TARGET, crystal, 7_000L, + dimId -> "Body-" + dimId, HOME, Double.POSITIVE_INFINITY, true); } /** The column, in density-light-years, that the shipped threshold sits at. */ diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java index 2b2106d1f..41a478c61 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/PlanetRealizationTest.java @@ -57,16 +57,21 @@ private static UniverseRegistry registryWithProceduralGalaxy() { /** * The seat of a system near the origin. * - *

    Probed one SUPER-CELL at a time, never cell by cell: a star's seat is one cell in a cube of + *

    Probed one TERRITORY at a time, never cell by cell: a star's seat is one cell in a cube of * tens of millions, so a sweep of adjacent cells finds nothing however full the galaxy is. The - * partition is the thing to walk, and it is what the generator itself walks.

    + * partition is the thing to walk, and it is what the generator itself walks — and it is asked + * what the whole territory HOLDS, because a territory is divided uniformly and resolving its + * corner point would sample one seat in k-cubed and read a full galaxy as an empty one.

    */ private static GalacticCoord systemAnchor(UniverseRegistry reg) { for (long i = 0; i <= 8; i++) { - Optional anchor = reg.anchorForCell( - GalacticCoord.ofSectorLocal(i * SPACING, 0L, 0L, 0L, 0L, 0L)); - if (anchor.isPresent()) { - return anchor.get(); + for (GalacticCoord anchor : reg.anchorsInTerritory( + GalacticCoord.ofSectorLocal(i * SPACING, 0L, 0L, 0L, 0L, 0L), 64)) { + // A system with a STAR. A territory's seats include unbound worlds, which hold one + // world and no retinue - everything below is about a body that ORBITS something. + if (reg.starAt(anchor).isPresent()) { + return anchor; + } } } return null; @@ -95,13 +100,10 @@ private static GalacticCoord findLandableCell(UniverseRegistry reg) { */ private static SystemBody[] findPlanetWithMoon(UniverseRegistry reg) { for (long i = 0; i <= 8; i++) { - Optional seat = reg.anchorForCell( - GalacticCoord.ofSectorLocal(i * SPACING, 0L, 0L, 0L, 0L, 0L)); - if (!seat.isPresent()) { - continue; - } + for (GalacticCoord seat : reg.anchorsInTerritory( + GalacticCoord.ofSectorLocal(i * SPACING, 0L, 0L, 0L, 0L, 0L), 64)) { SystemBody parent = null; - for (SystemBody b : reg.systemBodiesAt(seat.get())) { + for (SystemBody b : reg.systemBodiesAt(seat)) { if (b.kind() != SystemBodyKind.MOON && b.kind().canDescend()) { parent = b; } else if (b.kind() == SystemBodyKind.MOON && parent != null @@ -109,6 +111,7 @@ private static SystemBody[] findPlanetWithMoon(UniverseRegistry reg) { return new SystemBody[] {parent, b}; } } + } } return new SystemBody[] {null, null}; } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java index 3760c9482..c992dd173 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/StarClusterTest.java @@ -229,8 +229,14 @@ public void aClusterHoldsFarMoreStarsThanTheFieldAroundit() { found.centreSuperY(), found.centreSuperZ(), s); assertTrue("a cluster must be denser than the field beside it (" + inside + " vs " + outside + ") for " + found, inside > outside); - assertTrue("and the field outside it must hold at most the one seat a coarse cell allows", - outside <= 1); + // The field outside is no longer "one seat per coarse cell" — every territory is divided + // uniformly so that a free-floating population can be counted — so what is pinned is the + // CONTRAST that makes a cluster a cluster. A cluster subdivides k times further and is meant + // to be k-cubed times denser; requiring only a factor of k keeps this a tripwire against the + // contrast collapsing rather than a re-measurement of the draw's variance. + assertTrue("a cluster must out-hold the field beside it by at least its own subdivision (" + + inside + " vs " + outside + " at k=" + found.subdivision() + ")", + inside >= outside * found.subdivision()); } @Test diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java index 0cbd774cf..de6f92575 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/SystemRetinueTest.java @@ -83,14 +83,18 @@ private static List anchors(ClusteredGalaxyGenerator g, long seed for (long sx = -supercells; sx <= supercells; sx++) { for (long sy = -supercells; sy <= supercells; sy++) { for (long sz = -supercells; sz <= supercells; sz++) { - Optional a = g.anchorAt(seed, - cell(sx * minSpacing, sy * minSpacing, sz * minSpacing)); - if (!a.isPresent() || !seen.add(a.get().cellKey())) { - continue; - } - Optional sys = g.systemAt(seed, a.get()); - if (sys.isPresent() && sys.get().star().isPresent()) { - out.add(a.get()); + // What the TERRITORY holds, not what its corner point resolves to. The lattice is + // divided uniformly, so a point probe samples one seat in k-cubed — a sweep built + // on one reads a populated field as an almost empty one. + for (GalacticCoord a : g.anchorsInTerritory(seed, + cell(sx * minSpacing, sy * minSpacing, sz * minSpacing), 64)) { + if (!seen.add(a.cellKey())) { + continue; + } + Optional sys = g.systemAt(seed, a); + if (sys.isPresent() && sys.get().star().isPresent()) { + out.add(a); + } } } } diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeConeSurveyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeConeSurveyTest.java new file mode 100644 index 000000000..0648e265f --- /dev/null +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeConeSurveyTest.java @@ -0,0 +1,498 @@ +package zmaster587.advancedRocketry.test.unit; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.junit.After; +import org.junit.Test; + +import zmaster587.advancedRocketry.api.ARConfiguration; +import zmaster587.advancedRocketry.api.Constants; +import zmaster587.advancedRocketry.api.dimension.solar.StellarBody; +import zmaster587.advancedRocketry.navigation.CrystalMemory; +import zmaster587.advancedRocketry.space.GalacticCoord; +import zmaster587.advancedRocketry.universe.ClusteredGalaxyGenerator; +import zmaster587.advancedRocketry.universe.ConeWalk; +import zmaster587.advancedRocketry.universe.EmptyGalaxyGenerator; +import zmaster587.advancedRocketry.universe.GalaxyGenConfig; +import zmaster587.advancedRocketry.universe.RegionScan; +import zmaster587.advancedRocketry.universe.StellarMagnitude; +import zmaster587.advancedRocketry.universe.SystemBody; +import zmaster587.advancedRocketry.universe.SystemBodyKind; +import zmaster587.advancedRocketry.universe.TelescopeScan; +import zmaster587.advancedRocketry.universe.UniverseRegistry; +import zmaster587.advancedRocketry.universe.UniverseScale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Contract tests for what a telescope LOOKS AT and what it can SEE. + * + *

    Two claims, and they are the whole of the redesign. A survey is a cone with its apex at + * the instrument rather than a box of coordinates with no observer; and what it finds is bounded by + * brightness rather than by a configured horizon, so its reach is derived from its aperture + * and is a different distance for a red dwarf than for a blue giant.

    + * + *

    These pin player-facing promises: a better aperture reaches farther, dust costs reach the same + * way distance does, a starless world is not something a telescope finds, a look reports its whole + * territory rather than a sample of it, and the detection stage is genuinely cheaper than the + * characterisation stage. They do not pin the sweep order, the tick formula or the storage shape.

    + */ +public class TelescopeConeSurveyTest { + + private static final GalacticCoord HOME = GalacticCoord.ofSectorLocal(0, 0, 0, 0, 0, 0); + private static final long SEED = 0xC0FFEEL; + private static final long STEP = GalaxyGenConfig.DEFAULT_MIN_SPACING; + + @After + public void resetSeams() { + UniverseRegistry.setGenerator(null); + UniverseRegistry.setStarLookup(null); + } + + private static GalacticCoord cell(long x, long y, long z) { + return GalacticCoord.ofSectorLocal(x, y, z, 0L, 0L, 0L); + } + + /** A star of a stated bulk — the only two numbers its brightness is made of. */ + private static StellarBody starOf(int id, float sizeSuns, int temperatureUnits) { + StellarBody s = new StellarBody(); + s.setId(id); + s.setName("Star-" + id); + s.setSize(sizeSuns); + s.setTemperature(temperatureUnits); + return s; + } + + private static List archetypes() { + return GalaxyGenConfig.defaults().starTypes; + } + + // ── the photometry ──────────────────────────────────────────────────────── + + @Test + public void aStarsBrightnessIsMadeOfItsSizeAndItsTemperature() { + // The Stefan-Boltzmann law, and the fourth power is the whole reason the sky's brightness is + // so unlike its population: a blue star is 0.13 % of the stars and outshines a red dwarf by + // nearly four orders. Both stock archetypes, against the figures the design was sized from. + double redDwarf = StellarMagnitude.luminositySuns(0.8d, 40); + double blueGiant = StellarMagnitude.luminositySuns(2.0d, 220); + + assertEquals("a mid-band red dwarf is about a sixtieth of a Sun", 0.0164d, redDwarf, 0.001d); + assertEquals("a mid-band blue giant is about ninety Suns", 93.7d, blueGiant, 0.5d); + assertEquals("and the Sun is one Sun", 1d, + StellarMagnitude.luminositySuns(1d, StellarMagnitude.SOLAR_TEMPERATURE_UNITS), 1e-9d); + } + + @Test + public void howFarAStarCanBeSeenIsTheThingAnApertureDecides() { + // The claim a configured horizon could not make: ONE instrument reaches eighty times farther + // for a blue giant than for a red dwarf, so no single number of light years describes it. + double redDwarf = StellarMagnitude.luminositySuns(0.8d, 40); + double blueGiant = StellarMagnitude.luminositySuns(2.0d, 220); + + double dwarfReach = StellarMagnitude.detectionRangeLightYears(redDwarf, 10d); + double giantReach = StellarMagnitude.detectionRangeLightYears(blueGiant, 10d); + + assertEquals("a red dwarf at the tenth magnitude reaches ~45 ly", 45d, dwarfReach, 2d); + assertEquals("a blue giant at the same limit reaches ~3 400 ly", 3414d, giantReach, 50d); + + // Five magnitudes is a factor of a hundred in flux, hence ten in distance. That is the ladder + // a better instrument climbs, and it is derived rather than configured. + assertEquals("five magnitudes of aperture must be ten times the reach", + 10d * dwarfReach, StellarMagnitude.detectionRangeLightYears(redDwarf, 15d), 1d); + } + + @Test + public void dustCostsReachExactlyTheWayDistanceDoes() { + // Why a magnitude limit is the right bound: extinction is measured in the same unit, so dust + // and distance are two terms of ONE sum instead of two mechanics that have to be reconciled. + double sunLike = StellarMagnitude.luminositySuns(1.15d, 100); + double absolute = StellarMagnitude.absoluteMagnitude(sunLike); + + double clear = StellarMagnitude.apparentMagnitude(absolute, 200d, 0d); + double dusty = StellarMagnitude.apparentMagnitude(absolute, 200d, 2.5d); + + assertEquals("two and a half magnitudes of dust dim it by two and a half magnitudes", + clear + 2.5d, dusty, 1e-9d); + // Measured, not asserted round: a 1.15-Sun star at 200 ly stands at magnitude 8.47 in clear + // sky and 10.97 behind this cloud, so a tenth-magnitude instrument sees the one and not + // the other. The bracket is what makes the sum above a MECHANIC rather than arithmetic. + assertTrue("a star inside the aperture in clear sky must be outside it behind a cloud: " + + clear + " -> " + dusty, + clear < 10d && dusty > 10d); + } + + @Test + public void aStarDescribedOnlyByItsSizeIsReadAtTheSunsTemperature() { + // A pack may state a star's size and say nothing about its temperature, and zero raised to + // the fourth power is a star that emits nothing — so the pack would have authored an + // invisible sun and found out by pointing a telescope at empty sky. Zero means UNSTATED. + StellarBody unstated = new StellarBody(); + unstated.setSize(1f); + + assertEquals("a Sun-sized star with no stated temperature is a Sun", 1d, + StellarMagnitude.luminositySuns(unstated), 1e-9d); + + // And a thing that really is dark says so, rather than being inferred from a missing number. + StellarBody hole = new StellarBody(); + hole.setBlackHole(true); + assertEquals("a black hole emits nothing a survey in the visible could catch", 0d, + StellarMagnitude.luminositySuns(hole), 0d); + } + + // ── the shape ───────────────────────────────────────────────────────────── + + @Test + public void aPointingIsAConeAndEveryLookLiesInsideIt() { + // The shape itself: everything the survey looks at is within the half-angle of the axis, and + // within the reach. A box could not state either sentence, because it has no apex. + double halfAngle = Math.toRadians(15d); + ConeWalk cone = ConeWalk.aimed(HOME, 1, 0, 0, halfAngle, 40 * STEP, STEP); + + assertTrue("a pointing worth walking must hold more than its axis", cone.totalLooks() > 40); + for (int i = 0; i < cone.totalLooks(); i++) { + GalacticCoord look = cone.lookAt(i); + double axial = look.sectorX(); + double across = Math.hypot(look.sectorY(), look.sectorZ()); + assertTrue("a look must lie in front of the instrument, not behind it: " + look.cellKey(), + axial > 0d); + // One stride of slack: a look sits on a lattice, so the cell it rounds to can be half a + // stride outside the mathematical cone without the pointing having widened. + assertTrue("a look must lie inside the cone: " + look.cellKey() + " is " + + Math.toDegrees(Math.atan2(across, axial)) + " degrees off axis", + across <= axial * Math.tan(halfAngle) + STEP); + assertTrue("and inside the reach", axial <= 40 * STEP); + } + } + + @Test + public void aWiderPatchOfSkyIsMoreSurveyAndTheGrowthIsTheSquareOfTheAngle() { + // What an operator is trading when he opens the aperture up. Stated because it is the number + // that decides whether a configuration is playable: doubling the opening quadruples the work. + long reach = 200 * STEP; + int narrow = ConeWalk.aimed(HOME, 0, 0, 1, Math.toRadians(5d), reach, STEP).totalLooks(); + int wide = ConeWalk.aimed(HOME, 0, 0, 1, Math.toRadians(10d), reach, STEP).totalLooks(); + + System.out.println("a 200-territory pointing holds " + narrow + " looks at 5 degrees and " + + wide + " at 10"); + assertTrue("twice the opening must be about four times the survey: " + narrow + " -> " + wide, + wide > narrow * 3 && wide < narrow * 5); + } + + @Test + public void aPointingIsWalkedOutwardsSoAnAbortedSurveyIsAShorterCone() { + // Not cosmetic: a survey may be stopped at any point, and what a half-finished one has + // covered must be the NEAR sky rather than a scatter through the far. + ConeWalk cone = ConeWalk.aimed(HOME, 0, 1, 0, Math.toRadians(20d), 30 * STEP, STEP); + + long deepestSoFar = 0; + for (int i = 0; i < cone.totalLooks(); i++) { + long depth = cone.lookAt(i).sectorY(); + assertTrue("the walk must never step back towards the instrument: " + depth + + " after " + deepestSoFar, depth >= deepestSoFar); + deepestSoFar = depth; + } + assertTrue("the walk must reach the pointing's own depth", deepestSoFar >= 29 * STEP); + } + + @Test + public void aPointingSurvivesTheChunkItStartedIn() { + // The save contract: a pointing is an apex, a direction and an opening, and all three come + // back — a survey that reloaded aimed somewhere else would quietly resume over other sky. + ConeWalk cone = ConeWalk.aimed(cell(11, -3, 7), 2, -5, 1, Math.toRadians(3d), 60 * STEP, STEP); + net.minecraft.nbt.NBTTagCompound nbt = new net.minecraft.nbt.NBTTagCompound(); + cone.writeToNBT(nbt); + + ConeWalk back = ConeWalk.readFromNBT(nbt); + assertNotNull("a saved pointing must come back", back); + assertEquals("aimed from the same place", cone.apex().cellKey(), back.apex().cellKey()); + assertEquals("at the same opening", cone.halfAngleRadians(), back.halfAngleRadians(), 1e-12d); + assertEquals("over the same sky", cone.totalLooks(), back.totalLooks()); + assertEquals("and every look must land where it landed before", + cone.lookAt(cone.totalLooks() / 2).cellKey(), + back.lookAt(back.totalLooks() / 2).cellKey()); + } + + // ── what a look registers ───────────────────────────────────────────────── + + /** A registry holding one star of a stated bulk, seated {@code lightYears} away along +X. */ + private static UniverseRegistry oneStarAt(double lightYears, float sizeSuns, int temperature) { + UniverseRegistry.setGenerator(new EmptyGalaxyGenerator()); + UniverseRegistry.setStarLookup(id -> starOf(id, sizeSuns, temperature)); + + UniverseRegistry registry = new UniverseRegistry(); + GalacticCoord seat = cell(UniverseScale.cellsForLightYears(lightYears), 0, 0); + registry.place(seat, 7); + registry.addPoi(SystemBody.fixedAt(seat, SystemBodyKind.STAR, Constants.INVALID_PLANET, 7)); + registry.addPoi(SystemBody.fixedAt(seat, SystemBodyKind.PLANET, 701, 7)); + return registry; + } + + private static GalacticCoord seatAt(double lightYears) { + return cell(UniverseScale.cellsForLightYears(lightYears), 0, 0); + } + + @Test + public void aStarInsideTheApertureRegistersAndOneBeyondItDoesNot() { + // THE mechanic. The same star, the same direction, the same instrument — and the only thing + // that decides whether the survey knows it is there is how far away it is. + double sunLike = StellarMagnitude.luminositySuns(1.15d, 100); + double reach = StellarMagnitude.detectionRangeLightYears(sunLike, 8d); + assertTrue("arrangement: a sun-like star at the shipped aperture must reach a useful way", + reach > 100d); + + UniverseRegistry near = oneStarAt(reach * 0.5d, 1.15f, 100); + assertEquals("a star well inside the aperture must register", 1, + TelescopeScan.detect(near, seatAt(reach * 0.5d), HOME, 8d).size()); + + UniverseRegistry far = oneStarAt(reach * 2d, 1.15f, 100); + assertEquals("and the same star twice its reach away must not", 0, + TelescopeScan.detect(far, seatAt(reach * 2d), HOME, 8d).size()); + } + + @Test + public void aBetterApertureFindsWhatAWorseOneCannot() { + // The progression axis the design replaces a config horizon with: the instrument improves, + // and the sky it can reach improves with it — without a number anywhere being raised. + double sunLike = StellarMagnitude.luminositySuns(1.15d, 100); + double justOutOfReach = StellarMagnitude.detectionRangeLightYears(sunLike, 8d) * 1.5d; + UniverseRegistry registry = oneStarAt(justOutOfReach, 1.15f, 100); + GalacticCoord seat = seatAt(justOutOfReach); + + assertTrue("arrangement: the star must be out of the shipped aperture's reach", + TelescopeScan.detect(registry, seat, HOME, 8d).isEmpty()); + assertFalse("a better aperture must find it without anything else changing", + TelescopeScan.detect(registry, seat, HOME, 13d).isEmpty()); + } + + @Test + public void aDetectionCarriesHowFarAwayAndHowBrightItLooked() { + // A detection is a fact about a LOOK and not about a system: the same star is a different + // detection from somewhere else, and the second stage needs both numbers to decide how much + // of it can be made out. + UniverseRegistry registry = oneStarAt(300d, 1.15f, 100); + List hits = TelescopeScan.detect(registry, seatAt(300d), HOME, 25d); + + assertEquals("arrangement: exactly one star to describe", 1, hits.size()); + TelescopeScan.Detection hit = hits.get(0); + assertEquals("it must know how far away it is", 300d, hit.distanceLightYears(), 5d); + assertEquals("and how bright it looked, which is the two together", + StellarMagnitude.apparentMagnitude( + StellarMagnitude.absoluteMagnitude(StellarMagnitude.luminositySuns(1.15d, 100)), + hit.distanceLightYears(), 0d), + hit.apparentMagnitude(), 1e-6d); + } + + @Test + public void aStarlessWorldIsNotSomethingATelescopeFinds() { + // Physics the mechanic inherits rather than a rule someone wrote: an unbound world emits + // nothing, so no aperture registers one. Finding a rogue planet is a thing you do by GOING + // there, and that is what makes the void worth flying into rather than surveying from home. + UniverseRegistry.setGenerator(new EmptyGalaxyGenerator()); + UniverseRegistry.setStarLookup(id -> null); + UniverseRegistry registry = new UniverseRegistry(); + GalacticCoord seat = cell(UniverseScale.cellsForLightYears(20d), 0, 0); + registry.place(seat, 3); + registry.addPoi(SystemBody.fixedAt(seat, SystemBodyKind.ROGUE_PLANET, 301, 3)); + + assertTrue("a starless world must never register, at any aperture", + TelescopeScan.detect(registry, seat, HOME, 40d).isEmpty()); + + // And the discriminator: what is unreachable by LIGHT is still reachable by being there. + CrystalMemory crystal = new CrystalMemory(); + assertTrue("an instrument standing in it must still be able to name it", + TelescopeScan.resolveCell(registry, seat, crystal, 1_000L, id -> "Body-" + id) > 0); + } + + // ── detection is not characterisation ───────────────────────────────────── + + /** The real generator, counting the two questions separately. */ + private static final class SplitCountingGenerator + implements zmaster587.advancedRocketry.universe.IGalaxyGenerator { + + private final ClusteredGalaxyGenerator real; + int territoryQueries; + int bodyQueries; + + SplitCountingGenerator(GalaxyGenConfig config) { + this.real = new ClusteredGalaxyGenerator(config); + } + + @Override + public Optional systemAt( + long seed, GalacticCoord coord) { + return real.systemAt(seed, coord); + } + + @Override + public java.util.Map + systemsInRegion(long seed, GalacticCoord min, GalacticCoord max) { + return real.systemsInRegion(seed, min, max); + } + + @Override + public Optional anchorAt(long seed, GalacticCoord cell) { + return real.anchorAt(seed, cell); + } + + @Override + public List anchorsInTerritory(long seed, GalacticCoord cell, int limit) { + territoryQueries++; + return real.anchorsInTerritory(seed, cell, limit); + } + + @Override + public List bodiesFor(long seed, GalacticCoord systemCoord) { + bodyQueries++; + return real.bodiesFor(seed, systemCoord); + } + + @Override + public int minSpacingCells() { + return real.minSpacingCells(); + } + + @Override + public Optional tuning() { + return real.tuning(); + } + } + + @Test + public void detectionAsksTheCheapQuestionWithoutPayingForTheExpensiveOne() { + // The split the whole redesign turns on. These were one call, so the cheap question ("is + // anything there") could never be asked without building every body of every system it + // found. A survey spends its looks on the first stage, so the first stage must not touch + // the second — and the only way to state that is to count. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + SplitCountingGenerator counting = new SplitCountingGenerator(config); + UniverseRegistry.setGenerator(counting); + UniverseRegistry.setStarLookup(id -> starOf(id, 1f, 100)); + UniverseRegistry registry = new UniverseRegistry(); + registry.bindWorldSeed(SEED); + + int found = 0; + for (int i = 1; i <= 40; i++) { + found += TelescopeScan.detect(registry, cell((long) i * STEP, 0, 0), HOME, 12d).size(); + } + + System.out.println("40 detection looks found " + found + " systems, asking " + + counting.territoryQueries + " territory questions and " + counting.bodyQueries + + " body questions"); + assertTrue("arrangement: the sweep must have found something to describe", found > 0); + assertEquals("detection must never derive a single body", 0, counting.bodyQueries); + } + + @Test + public void oneLookOwesItsWholeTerritoryAndNotOneSeatOfIt() { + // The property that lets a survey stride by the territory while the field is divided more + // finely than that. Without it a sweep reports one seat in k-cubed and calls it the sky: at + // the shipped division that is 1.3 % of what is out there, reported as all of it. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + ClusteredGalaxyGenerator gen = new ClusteredGalaxyGenerator(config); + + int byTerritory = 0; + int byPoint = 0; + for (long i = -6; i <= 6; i++) { + for (long j = -6; j <= 6; j++) { + GalacticCoord corner = cell(i * STEP, j * STEP, 0); + byTerritory += gen.anchorsInTerritory(SEED, corner, 64).size(); + byPoint += gen.anchorAt(SEED, corner).isPresent() ? 1 : 0; + } + } + + System.out.println("169 territories hold " + byTerritory + " systems; resolving their corner " + + "points alone would have reported " + byPoint); + assertTrue("arrangement: the field must hold something", byTerritory > 0); + assertTrue("asking the territory must find strictly more than sampling one point of it: " + + byTerritory + " vs " + byPoint, + byTerritory > byPoint); + } + + // ── what the shipped instrument costs ───────────────────────────────────── + + @Test + public void theShippedApertureIsAffordableAndItsFindingsFitOnACrystal() { + // THE acceptance measurement, and the numbers are stated in the units of the goal rather + // than in whatever the work happened to produce: a full-depth pointing at the shipped + // aperture must hold under 200 000 looks, register a number of systems a crystal can carry, + // and cost well under a second of CPU spread over its steps. + GalaxyGenConfig config = GalaxyGenConfig.defaults(); + UniverseRegistry.setGenerator(new ClusteredGalaxyGenerator(config)); + UniverseRegistry.setStarLookup(id -> starOf(id, 1f, 100)); + UniverseRegistry registry = new UniverseRegistry(); + registry.bindWorldSeed(SEED); + + RegionScan.Tuning shipped = new RegionScan.Tuning( + ARConfiguration.DEFAULT_TELESCOPE_LIMITING_MAGNITUDE, archetypes(), + Math.toRadians(ARConfiguration.DEFAULT_TELESCOPE_CONE_HALF_ANGLE_DEGREES), + ARConfiguration.DEFAULT_TELESCOPE_SCAN_MAX_CELLS, + ARConfiguration.DEFAULT_TELESCOPE_SCAN_BASE_TICKS, + ARConfiguration.DEFAULT_TELESCOPE_SCAN_CELLS_PER_STEP, + config.minSpacing); + + RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, shipped.maxRangeSteps(), 0L, shipped); + int looks = scan.totalCells(); + + long startedAt = System.nanoTime(); + int detections = 0; + for (int i = 0; i < looks; i++) { + detections += TelescopeScan.detect(registry, scan.cellAt(i), HOME, + shipped.limitMagnitude()).size(); + } + long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000L; + long steps = (looks + shipped.cellsPerStep() - 1) / shipped.cellsPerStep(); + + System.out.println("the shipped instrument reaches " + + String.format("%.0f", shipped.maxRangeLightYears()) + " ly (" + + shipped.maxRangeSteps() + " territories); a full pointing is " + looks + + " looks in " + steps + " steps (" + (steps * shipped.baseTicks() / 20L) + + " s of clear night), registered " + detections + " systems, walked in " + + elapsedMs + " ms"); + + assertTrue("a full pointing must stay under the walk ceiling: " + looks, + looks <= ARConfiguration.DEFAULT_TELESCOPE_SCAN_MAX_CELLS); + assertTrue("and must be a real survey rather than a token one: " + looks, looks > 1_000); + assertTrue("what it registers must fit on a crystal: " + detections + " systems", + detections <= 1_500); + assertTrue("arrangement: a pointing that finds nothing would pass every bound above", + detections > 0); + assertTrue("and the walk must cost well under a second of CPU: " + elapsedMs + " ms", + elapsedMs < 2_000L); + } + + @Test + public void anApertureTooGoodForTheWalkBudgetShortensTheReachRatherThanRefusingToLook() { + // UNREASONABLE IS NOT IMPOSSIBLE. An operator who configures an aperture that would hold more + // looks than the ceiling affords gets a shallower pointing, not an instrument that will not + // point — he sees the near sky and can point again. + RegionScan.Tuning greedy = new RegionScan.Tuning(25d, archetypes(), Math.toRadians(5d), + 5_000, 20, 100, STEP); + assertTrue("arrangement: this aperture must reach absurdly far", + greedy.maxRangeLightYears() > 100_000d); + + RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, greedy.maxRangeSteps(), 0L, greedy); + + assertTrue("the survey must fit under the ceiling: " + scan.totalCells(), + scan.totalCells() <= 5_000); + assertTrue("and must still be a survey rather than a single look", scan.totalCells() > 1); + assertTrue("its reach must have been SHORTENED, which is what a budget can do to a horizon", + scan.distanceCells() < greedy.maxRangeSteps() * STEP); + } + + @Test + public void aGeneratorWithNoStarsGivesAnInstrumentNothingToReach() { + // The honest zero. An empty universe has nothing to see, so a survey of it is instantly + // complete rather than long and fruitless — and the reach says so rather than inventing one. + RegionScan.Tuning empty = new RegionScan.Tuning(20d, new ArrayList<>(), Math.toRadians(1d), + 1_000, 20, 10, STEP); + assertEquals("an aperture pointed at a sky with no star types reaches nothing", 0d, + empty.maxRangeLightYears(), 0d); + assertEquals("which is still a pointing, of one territory", 1, empty.maxRangeSteps()); + } +} diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java index e124c5f58..8cb3f0b28 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeRegionScanTest.java @@ -15,6 +15,7 @@ import zmaster587.advancedRocketry.universe.InfoTier; import zmaster587.advancedRocketry.universe.RegionScan; import zmaster587.advancedRocketry.universe.SystemBody; +import zmaster587.advancedRocketry.universe.StellarMagnitude; import zmaster587.advancedRocketry.universe.SystemBodyKind; import zmaster587.advancedRocketry.universe.TelescopeScan; import zmaster587.advancedRocketry.universe.UniverseRegistry; @@ -51,9 +52,41 @@ public class TelescopeRegionScanTest { */ private static final long STEP = GalaxyGenConfig.DEFAULT_MIN_SPACING; - /** Reach 50 light years, a 3×3×3 patch of territories, room for it, 100 ticks a step + 50 a ly. */ + /** The star types the sky can produce — what an aperture's reach is derived against. */ + private static java.util.List archetypes() { + return GalaxyGenConfig.defaults().starTypes; + } + + /** + * The aperture that reaches exactly {@code lightYears} — the magnitude law run BACKWARDS against + * the brightest star the stock sky holds. + * + *

    Written this way so a fixture can still say "a survey that reaches fifty light years" while + * the instrument is configured by what it can SEE. Inverting the production formula rather than + * hard-coding a magnitude also means these fixtures follow the star table: retune the sky's + * brightest archetype and the fixture still reaches fifty light years.

    + */ + private static double apertureReaching(double lightYears) { + double brightest = 0d; + for (GalaxyGenConfig.StarType type : archetypes()) { + brightest = Math.max(brightest, + StellarMagnitude.luminositySuns(type.maxSize, type.temperature)); + } + double parsecs = lightYears / StellarMagnitude.LIGHT_YEARS_PER_PARSEC; + return StellarMagnitude.absoluteMagnitude(brightest) + 5d * Math.log10(parsecs / 10d); + } + + /** + * A pointing that reaches 50 light years, one degree wide, 100 ticks a step, two looks a step. + * + *

    One degree is narrower than the lattice is coarse for the first fifty-odd territories, so + * every look of a shallow pointing lands exactly on the axis. That is the geometry and not a + * simplification — a cone IS a line until it is wider than the spacing of what it walks — and it + * makes a fixture's looks predictable without pinning the sweep order.

    + */ private static RegionScan.Tuning tuning() { - return new RegionScan.Tuning(50d, 1, 512, 100, 50d, 2, STEP); + return new RegionScan.Tuning(apertureReaching(50d), archetypes(), Math.toRadians(1d), + 512, 100, 2, STEP); } private static StellarBody star(int id) { @@ -133,8 +166,9 @@ public void theHorizonIsTheConfiguredReach() { public void oneStepNeverResolvesMoreThanItsCellBudget() { // The structural guard against reading an endless procedural universe off one instrument: // a survey may cover a large region, but never in one step. - RegionScan.Tuning wide = new RegionScan.Tuning(50d, 2, 1000, 100, 50d, 3, STEP); - RegionScan scan = RegionScan.directed(HOME, 1, 1, 0, 3, 0L, wide); + RegionScan.Tuning wide = new RegionScan.Tuning(apertureReaching(200d), archetypes(), + Math.toRadians(20d), 1000, 100, 3, STEP); + RegionScan scan = RegionScan.directed(HOME, 1, 1, 0, wide.maxRangeSteps(), 0L, wide); assertTrue("the fixture must be a region worth sweeping", scan.totalCells() > 3); assertEquals("a step may never resolve more cells than its budget", @@ -144,8 +178,9 @@ public void oneStepNeverResolvesMoreThanItsCellBudget() { @Test public void aRegionNeverExceedsItsCeiling() { // Ask for a 9×9×9 region with room for 27 cells and the ceiling wins. - RegionScan.Tuning greedy = new RegionScan.Tuning(50d, 4, 27, 100, 50d, 2, STEP); - RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, 3, 0L, greedy); + RegionScan.Tuning greedy = new RegionScan.Tuning(apertureReaching(200d), archetypes(), + Math.toRadians(20d), 27, 100, 2, STEP); + RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, greedy.maxRangeSteps(), 0L, greedy); assertTrue("a survey may never cover more than its ceiling: " + scan.totalCells(), scan.totalCells() <= 27); @@ -154,7 +189,7 @@ public void aRegionNeverExceedsItsCeiling() { @Test public void aSweepWorksThroughItsRegionAndFinishes() { // The automation the instrument exists for: one aim, then it works through the patch. - RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, 2, 0L, tuning()); + RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, 8, 0L, tuning()); int total = scan.totalCells(); assertTrue("the fixture must need more than one step", total > scan.cellsPerStep()); @@ -182,7 +217,7 @@ public void nothingIsResolvedBeforeTheStepIsDue() { @Test public void everyCellOfTheRegionIsVisitedExactlyOnce() { - RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, 2, 0L, tuning()); + RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, 8, 0L, tuning()); java.util.Set seen = new java.util.HashSet<>(); for (int i = 0; i < scan.totalCells(); i++) { assertTrue("the sweep order must not repeat a cell: " + scan.cellAt(i).cellKey(), @@ -208,20 +243,28 @@ public void aSweepStridesByOneStarsTerritoryRatherThanByCells() { } @Test - public void theLocalRadarWalksCellsNotTerritories() { - // The other half of the same decision: close to home the cells ARE the granularity — the - // planet in the next cell is a different destination from its star — so the radar keeps a - // cell stride while the directed survey strides by stars. + public void theLocalRadarWatchesTheNeighbouringTerritories() { + // The passive half. It used to walk CELL BY CELL, on the ground that near home the cells are + // the granularity — which bought nothing, because one look already yields every body of the + // system that owns it, and no radius a cell-strided box could afford ever reached a + // NEIGHBOUR. Two cells was a fifth of the way to the innermost planet of the system the + // instrument was already standing in. A neighbourhood is measured in neighbours. RegionScan radar = RegionScan.local(HOME, 1, 0L, tuning()); - assertEquals("the local radar walks cell by cell", 1L, radar.strideCells()); - assertEquals("a radius of one cell is a 3x3x3 neighbourhood", 27, radar.totalCells()); + assertEquals("the local radar walks by star territories", STEP, radar.strideCells()); + assertEquals("a radius of one territory is the 27 around and including home", + 27, radar.totalCells()); boolean looksAtHome = false; + boolean reachesANeighbour = false; for (int i = 0; i < radar.totalCells(); i++) { - looksAtHome |= radar.cellAt(i).cellKey().equals(HOME.cellKey()); + GalacticCoord look = radar.cellAt(i); + looksAtHome |= look.cellKey().equals(HOME.cellKey()); + reachesANeighbour |= Math.abs(look.sectorX()) >= STEP; } - assertTrue("and it must look at the cell the instrument is standing in", looksAtHome); + assertTrue("it must look at the cell the instrument is standing in", looksAtHome); + assertTrue("and it must actually reach a neighbouring territory, which is the whole point", + reachesANeighbour); } @Test @@ -229,8 +272,8 @@ public void aRegionWithMoreLooksThanCanBeWalkedIsREFUSEDratherThanClamped() { // A survey is walked by an int cursor, and its look count used to be CLAMPED to fit one. A // clamped count does not make the sweep long — it makes it report itself complete at 2·10⁹ // looks with the rest of the region never visited, and progress read 100 % while the sky was - // untouched. The local radar is the reachable route: its radius is a config number and a cell - // stride cubes it, so ~1 300 cells of radius is already past an int. + // untouched. The local radar is the reachable route: its radius is a config number and the + // box cubes it, so ~1 300 of radius is already past an int. try { RegionScan.local(HOME, 2_000, 0L, tuning()); fail("a region of (2*2000+1)^3 looks cannot be walked and must be refused, not clamped"); @@ -301,7 +344,10 @@ private UniverseRegistry threeSystems() { registry.addPoi(SystemBody.fixedAt(inner, SystemBodyKind.STAR, Constants.INVALID_PLANET, 4)); registry.addPoi(SystemBody.fixedAt(inner, SystemBodyKind.PLANET, 401, 4)); - GalacticCoord edge = cell(5 * STEP + 7, STEP - 3, 0); // found by the look at the corner + // Two territories out, not three: the registry indexes ONE stored anchor per super-cell, so + // a second seat in the same territory as `inner` would silently displace it and this fixture + // would be testing a system it had already dropped. + GalacticCoord edge = cell(2 * STEP + 7, 0, 0); // found by the look two shells in registry.place(edge, 5); registry.addPoi(SystemBody.fixedAt(edge, SystemBodyKind.PLANET, 501, 5)); @@ -311,8 +357,8 @@ private UniverseRegistry threeSystems() { return registry; } - /** The survey the fixture is built around: 4 territories out along +X, one territory wide. */ - private RegionScan boxAroundFourthSector() { + /** The survey the fixture is built around: a pointing four territories deep along +X. */ + private RegionScan pointingFourTerritoriesOut() { return RegionScan.directed(HOME, 1, 0, 0, 4, 0L, tuning()); } @@ -327,7 +373,7 @@ public void whatIsInTheRegionIsLearnedAndWhatIsOutsideItIsNot() { UniverseRegistry registry = threeSystems(); CrystalMemory crystal = new CrystalMemory(); - surveyAll(registry, boxAroundFourthSector(), crystal, 7_000L); + surveyAll(registry, pointingFourTerritoriesOut(), crystal, 7_000L); assertNotNull("the body the instrument was pointed at must be learned", crystal.forBody(401)); assertNotNull("so must the one at the edge of the same region", crystal.forBody(501)); @@ -341,7 +387,7 @@ public void aSurveyWritesTheBODIESItResolved() { UniverseRegistry registry = threeSystems(); CrystalMemory crystal = new CrystalMemory(); - surveyAll(registry, boxAroundFourthSector(), crystal, 7_000L); + surveyAll(registry, pointingFourTerritoriesOut(), crystal, 7_000L); CrystalEntry planet = crystal.forBody(401); assertNotNull("the region's planet must have its own address", planet); @@ -404,6 +450,16 @@ public java.util.Optional anchorAt(long seed, GalacticCoord cell) return real.anchorAt(seed, cell); } + @Override + public java.util.List anchorsInTerritory(long seed, GalacticCoord cell, + int limit) { + // Delegated rather than inherited ON PURPOSE. The default would answer with the single + // anchor at the point, so a wrapper that merely counted would have measured a survey + // that never enumerated a territory - the cheap answer to a question nobody asked. + queries++; + return real.anchorsInTerritory(seed, cell, limit); + } + @Override public java.util.List bodiesFor(long seed, GalacticCoord systemCoord) { queries++; @@ -432,8 +488,8 @@ public void aSurveyResolvesPerLookAndNeverWalksTheGalaxy() { registry.bindWorldSeed(0xC0FFEEL); // Aimed at the real reach, through the real config's own stride. - RegionScan.Tuning live = new RegionScan.Tuning(100d, 1, 512, 100, 50d, 4, - config.minSpacing); + RegionScan.Tuning live = new RegionScan.Tuning(apertureReaching(100d), archetypes(), + Math.toRadians(1d), 512, 100, 4, config.minSpacing); RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, live.maxRangeSteps(), 0L, live); int looks = scan.totalCells(); assertTrue("the fixture must be a real sweep", looks >= 27); @@ -441,8 +497,10 @@ public void aSurveyResolvesPerLookAndNeverWalksTheGalaxy() { CrystalMemory crystal = new CrystalMemory(); TelescopeScan.resolveBatch(registry, scan, 0, looks, crystal, 7_000L, dimId -> "Body-" + dimId); - // A handful of questions per look: which system owns the cell, and what that system holds. - int budget = looks * 8; + // A handful of questions per look: what this territory holds, and what each of those + // systems is. The budget is per LOOK and not per system, so it has to allow for a territory + // that is divided - the bound that matters is that it does not grow with the GALAXY. + int budget = looks * 8 * TelescopeScan.MAX_SEATS_PER_LOOK; System.out.println("survey of " + looks + " looks asked the generator " + counting.queries + " questions (budget " + budget + ")"); assertTrue("a survey asked the generator " + counting.queries + " questions for " + looks @@ -494,9 +552,20 @@ public void aSystemAScanReportedIsFrozenAgainstALaterRetune() { UniverseRegistry registry = new UniverseRegistry(); registry.bindWorldSeed(0xC0FFEEL); - GalacticCoord looked = cell(7 * STEP, 3 * STEP, -5 * STEP); - GalacticCoord anchor = registry.anchorForCell(looked).orElse(null); - assertNotNull("arrangement: the looked-at cell must hold a system", anchor); + // SEARCHED rather than hardcoded. A territory is divided uniformly, so whether any given + // coordinate holds a system is a draw at one seat in k-cubed — a fixed cell was a fixture + // that happened to be occupied under one partition and is empty under the next. + GalacticCoord looked = null; + GalacticCoord anchor = null; + for (long i = 1; i <= 12 && anchor == null; i++) { + GalacticCoord probe = cell(i * STEP, 3 * STEP, -5 * STEP); + for (GalacticCoord found : registry.anchorsInTerritory(probe, 64)) { + looked = found; + anchor = found; + break; + } + } + assertNotNull("arrangement: the sweep must find a system to look at", anchor); String before = describe(registry, anchor); CrystalMemory crystal = new CrystalMemory(); @@ -542,7 +611,8 @@ public void whatASurveyFreezesIsMeasuredNotAssumed() { UniverseRegistry registry = new UniverseRegistry(); registry.bindWorldSeed(0xC0FFEEL); - RegionScan.Tuning live = new RegionScan.Tuning(100d, 1, 512, 100, 50d, 4, config.minSpacing); + RegionScan.Tuning live = new RegionScan.Tuning(apertureReaching(100d), archetypes(), + Math.toRadians(1d), 512, 100, 4, config.minSpacing); RegionScan scan = RegionScan.directed(HOME, 1, 0, 0, live.maxRangeSteps(), 0L, live); int looks = scan.totalCells(); CrystalMemory crystal = new CrystalMemory(); @@ -558,8 +628,9 @@ public void whatASurveyFreezesIsMeasuredNotAssumed() { System.out.println("survey of " + looks + " looks froze " + pins + " systems in " + elapsedMs + " ms; the universe save renders as " + bytes + " chars"); - assertTrue("a survey must not freeze more systems than it had looks (" + pins + " pins for " - + looks + " looks)", pins <= looks); + assertTrue("a survey must not freeze more systems than its looks could have found (" + pins + + " pins for " + looks + " looks)", + pins <= (long) looks * TelescopeScan.MAX_SEATS_PER_LOOK); assertTrue("arrangement: the sweep must have frozen something", pins > 0); } @@ -609,7 +680,7 @@ public void aSystemTheCrystalNeverHeardOfIsStillDiscovered() { InfoTier.TELESCOPE, 1_000L, 401)); assertNull("the fixture must start ignorant of the body under test", crystal.forBody(501)); - surveyAll(registry, boxAroundFourthSector(), crystal, 7_000L); + surveyAll(registry, pointingFourTerritoriesOut(), crystal, 7_000L); assertNotNull("a telescope discovers what nobody knew, or it discovers nothing", crystal.forBody(501)); @@ -622,7 +693,7 @@ public void whatATelescopeWritesIsCoarseAndDated() { UniverseRegistry registry = threeSystems(); CrystalMemory crystal = new CrystalMemory(); - surveyAll(registry, boxAroundFourthSector(), crystal, 7_000L); + surveyAll(registry, pointingFourTerritoriesOut(), crystal, 7_000L); CrystalEntry learned = crystal.forBody(401); assertNotNull(learned); @@ -637,7 +708,7 @@ public void aSweepWritesOnlyTheCellsItHasReached() { // instrument works, not all at the end. UniverseRegistry registry = threeSystems(); CrystalMemory crystal = new CrystalMemory(); - RegionScan scan = boxAroundFourthSector(); + RegionScan scan = pointingFourTerritoriesOut(); int firstCellWithContent = -1; for (int i = 0; i < scan.totalCells(); i++) { diff --git a/src/test/resources/universe/golden-corpus-v1.txt b/src/test/resources/universe/golden-corpus-v1.txt index d5f220d90..6348f3fb8 100644 --- a/src/test/resources/universe/golden-corpus-v1.txt +++ b/src/test/resources/universe/golden-corpus-v1.txt @@ -9,1756 +9,199 @@ scale ly=50000.0 cells=59129565454 backLy=50000.000000313135 cosmology tick=0 scaleFactor=1.0 cosmology tick=24000 scaleFactor=1.0000000000014915 cosmology tick=24000000 scaleFactor=1.0000000014914552 -seed 1 systems=27 - body -1230314_1893274_-2189019 -1230314_1893274_-2189019 kind=MOON orbit=0 radius=1.1108639180445716 starId=-1485753477 frame=false at=-46711,0,-41375 - body -1230314_1893274_-2189019 -1230314_1893274_-2189019 kind=ROGUE_PLANET orbit=0 radius=0.3271755891768876 starId=-1485753477 frame=true at=0,0,0 - body -1735123_-1297562_-3230849 -1735123_-1297562_-3230849 kind=ROGUE_PLANET orbit=0 radius=0.9122063191004484 starId=-457888649 frame=true at=0,0,0 - body -1839127_-930469_3024425 -1839127_-930469_3024425 kind=ROGUE_PLANET orbit=0 radius=1.2352684367448383 starId=-1123773025 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2967914_5695077_-2605367 kind=ASTEROID_BELT orbit=974 radius=0.0 starId=-423794625 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968046_5695074_-2605415 kind=PLANET orbit=255 radius=1.0703624393599125 starId=-423794625 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968054_5695071_-2605430 kind=PLANET orbit=162 radius=1.1112601957037147 starId=-423794625 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968066_5695072_-2605456 kind=ASTEROID_BELT orbit=44 radius=0.0 starId=-423794625 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968066_5695072_-2605466 kind=GAS_GIANT orbit=80 radius=5.032996057500449 starId=-423794625 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968066_5695072_-2605466 kind=MOON orbit=80 radius=0.3381283400377148 starId=-423794625 frame=false at=-429729,0,-381141 - body -2968074_5695072_-2605453 -2968066_5695072_-2605466 kind=MOON orbit=80 radius=0.3442873389359359 starId=-423794625 frame=false at=68040,0,-1111119 - body -2968074_5695072_-2605453 -2968073_5695072_-2605452 kind=STAR orbit=8 radius=79.49606685519218 starId=-423794626 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968074_5695072_-2605453 kind=STAR orbit=0 radius=0.0 starId=-423794625 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968075_5695072_-2605459 kind=PLANET orbit=31 radius=0.5085798172977589 starId=-423794625 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968080_5695072_-2605444 kind=PLANET orbit=57 radius=1.2180207924065842 starId=-423794625 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968141_5695075_-2605545 kind=GAS_GIANT orbit=609 radius=6.559618412306246 starId=-423794625 frame=true at=0,0,0 - body -2968074_5695072_-2605453 -2968198_5695072_-2605067 kind=STAR orbit=2168 radius=79.49606685519218 starId=-423794627 frame=true at=0,0,0 - body -3037636_-286450_6061438 -3037636_-286450_6061438 kind=MOON orbit=0 radius=0.20176280372394176 starId=-772485517 frame=false at=171483,0,-205174 - body -3037636_-286450_6061438 -3037636_-286450_6061438 kind=MOON orbit=0 radius=1.9715799845010111 starId=-772485517 frame=false at=-122688,0,-224913 - body -3037636_-286450_6061438 -3037636_-286450_6061438 kind=ROGUE_PLANET orbit=0 radius=1.1321185551403936 starId=-772485517 frame=true at=0,0,0 - body -3258554_1675500_3803298 -3258547_1675497_3803188 kind=ASTEROID_BELT orbit=588 radius=0.0 starId=-472927549 frame=true at=0,0,0 - body -3258554_1675500_3803298 -3258547_1675499_3803284 kind=GAS_GIANT orbit=84 radius=5.6716793111039 starId=-472927549 frame=true at=0,0,0 - body -3258554_1675500_3803298 -3258554_1675500_3803298 kind=STAR orbit=0 radius=0.0 starId=-472927549 frame=true at=0,0,0 - body -3258554_1675500_3803298 -3258555_1675500_3803299 kind=PLANET orbit=8 radius=1.0933516689678018 starId=-472927549 frame=true at=0,0,0 - body -3258554_1675500_3803298 -3258562_1675500_3803301 kind=ASTEROID_BELT orbit=46 radius=0.0 starId=-472927549 frame=true at=0,0,0 - body -3258554_1675500_3803298 -3258563_1675497_3803230 kind=MOON orbit=368 radius=0.527188535681306 starId=-472927549 frame=false at=45455,0,41574 - body -3258554_1675500_3803298 -3258563_1675497_3803230 kind=MOON orbit=368 radius=0.7108651577260094 starId=-472927549 frame=false at=-18923,0,3250 - body -3258554_1675500_3803298 -3258563_1675497_3803230 kind=PLANET orbit=368 radius=0.22596366099897897 starId=-472927549 frame=true at=0,0,0 - body -545630_6755069_2561467 -545471_6755071_2561465 kind=GAS_GIANT orbit=853 radius=10.105985698063467 starId=-1674485141 frame=true at=0,0,0 - body -545630_6755069_2561467 -545471_6755071_2561465 kind=MOON orbit=853 radius=0.27739774330825634 starId=-1674485141 frame=false at=-209250,0,1087654 - body -545630_6755069_2561467 -545471_6755071_2561465 kind=MOON orbit=853 radius=0.4474341471900287 starId=-1674485141 frame=false at=-417841,0,-2284502 - body -545630_6755069_2561467 -545611_6755062_2561721 kind=ASTEROID_BELT orbit=1364 radius=0.0 starId=-1674485141 frame=true at=0,0,0 - body -545630_6755069_2561467 -545624_6755069_2561468 kind=STAR orbit=34 radius=85.67155276358127 starId=-1674485142 frame=true at=0,0,0 - body -545630_6755069_2561467 -545630_6755069_2561467 kind=STAR orbit=0 radius=0.0 starId=-1674485141 frame=true at=0,0,0 - body -545630_6755069_2561467 -545634_6755067_2561379 kind=GAS_GIANT orbit=473 radius=4.353696915708663 starId=-1674485141 frame=true at=0,0,0 - body -545630_6755069_2561467 -545634_6755067_2561379 kind=MOON orbit=473 radius=0.23383540089451044 starId=-1674485141 frame=false at=-859794,0,-774510 - body -545630_6755069_2561467 -545646_6755070_2561489 kind=MOON orbit=143 radius=0.39785081213673035 starId=-1674485141 frame=false at=25172,0,-34862 - body -545630_6755069_2561467 -545646_6755070_2561489 kind=PLANET orbit=143 radius=0.2807380026340163 starId=-1674485141 frame=true at=0,0,0 - body -545630_6755069_2561467 -545654_6755068_2561424 kind=ASTEROID_BELT orbit=262 radius=0.0 starId=-1674485141 frame=true at=0,0,0 - body -755250_5247165_5302015 -755250_5247165_5302015 kind=ROGUE_PLANET orbit=0 radius=1.5423697863264323 starId=-157245369 frame=true at=0,0,0 - body -758984_1485681_3107299 -758984_1485681_3107299 kind=MOON orbit=0 radius=0.6401362407909844 starId=-726144233 frame=false at=-121535,0,421425 - body -758984_1485681_3107299 -758984_1485681_3107299 kind=MOON orbit=0 radius=1.7493057530044385 starId=-726144233 frame=false at=108838,0,-324430 - body -758984_1485681_3107299 -758984_1485681_3107299 kind=ROGUE_PLANET orbit=0 radius=2.484840931637559 starId=-726144233 frame=true at=0,0,0 - body 1159501_438377_2970042 1159501_438377_2970042 kind=ROGUE_PLANET orbit=0 radius=0.2438710079048173 starId=-875095761 frame=true at=0,0,0 - body 1502575_5241619_578848 1502575_5241619_578848 kind=ROGUE_PLANET orbit=0 radius=2.2738202904677567 starId=-440778097 frame=true at=0,0,0 - body 1915131_1673389_-1943074 1915131_1673389_-1943074 kind=MOON orbit=0 radius=0.21608366869246257 starId=-268045669 frame=false at=39280,0,26530 - body 1915131_1673389_-1943074 1915131_1673389_-1943074 kind=ROGUE_PLANET orbit=0 radius=0.5320514501890937 starId=-268045669 frame=true at=0,0,0 - body 2639269_-1847617_-2599924 2639190_-1847611_-2600049 kind=MOON orbit=791 radius=0.5916428926581476 starId=-1586685861 frame=false at=61277,0,-9439 - body 2639269_-1847617_-2599924 2639190_-1847611_-2600049 kind=PLANET orbit=791 radius=0.3444434132626236 starId=-1586685861 frame=true at=0,0,0 - body 2639269_-1847617_-2599924 2639267_-1847617_-2599925 kind=MOON orbit=11 radius=0.2025456744373965 starId=-1586685861 frame=false at=67016,0,-45851 - body 2639269_-1847617_-2599924 2639267_-1847617_-2599925 kind=MOON orbit=11 radius=0.422375278424855 starId=-1586685861 frame=false at=-18140,0,-51287 - body 2639269_-1847617_-2599924 2639267_-1847617_-2599925 kind=PLANET orbit=11 radius=0.29987989003936566 starId=-1586685861 frame=true at=0,0,0 - body 2639269_-1847617_-2599924 2639269_-1847617_-2599924 kind=STAR orbit=0 radius=0.0 starId=-1586685861 frame=true at=0,0,0 - body 2639269_-1847617_-2599924 2639272_-1847617_-2599928 kind=PLANET orbit=28 radius=0.369897875724714 starId=-1586685861 frame=true at=0,0,0 - body 2639269_-1847617_-2599924 2639274_-1847617_-2599896 kind=STAR orbit=152 radius=84.70957162857056 starId=-1586685864 frame=true at=0,0,0 - body 2639269_-1847617_-2599924 2639288_-1847617_-2599929 kind=STAR orbit=105 radius=84.70957162857056 starId=-1586685863 frame=true at=0,0,0 - body 2639269_-1847617_-2599924 2639505_-1847606_-2599928 kind=ASTEROID_BELT orbit=1265 radius=0.0 starId=-1586685861 frame=true at=0,0,0 - body 2639269_-1847617_-2599924 2641577_-1847617_-2605840 kind=STAR orbit=33958 radius=79.98081523776054 starId=-1586685862 frame=true at=0,0,0 - body 2646744_5580083_-2997664 2646704_5580086_-2997730 kind=ASTEROID_BELT orbit=414 radius=0.0 starId=-1182950741 frame=true at=0,0,0 - body 2646744_5580083_-2997664 2646744_5580083_-2997664 kind=STAR orbit=0 radius=0.0 starId=-1182950741 frame=true at=0,0,0 - body 2646744_5580083_-2997664 2646746_5580083_-2997663 kind=MOON orbit=13 radius=0.47444771458559326 starId=-1182950741 frame=false at=331593,0,-173844 - body 2646744_5580083_-2997664 2646746_5580083_-2997663 kind=PLANET orbit=13 radius=2.3360233067337304 starId=-1182950741 frame=true at=0,0,0 - body 2646744_5580083_-2997664 2646748_5580083_-2997658 kind=MOON orbit=39 radius=0.6828298515923701 starId=-1182950741 frame=false at=118099,0,49363 - body 2646744_5580083_-2997664 2646748_5580083_-2997658 kind=PLANET orbit=39 radius=1.7389342248982458 starId=-1182950741 frame=true at=0,0,0 - body 2646744_5580083_-2997664 2646762_5580082_-2997709 kind=MOON orbit=259 radius=0.5452012592227564 starId=-1182950741 frame=false at=-40362,0,-1763 - body 2646744_5580083_-2997664 2646762_5580082_-2997709 kind=PLANET orbit=259 radius=0.2127699482289579 starId=-1182950741 frame=true at=0,0,0 - body 2868006_4842033_4291324 2868006_4842033_4291324 kind=ROGUE_PLANET orbit=0 radius=1.7074633543397322 starId=-1484690657 frame=true at=0,0,0 - body 3210136_-1258479_1657446 3210128_-1258480_1657458 kind=MOON orbit=77 radius=0.2148191821741564 starId=-143401165 frame=false at=-53418,0,-141877 - body 3210136_-1258479_1657446 3210128_-1258480_1657458 kind=MOON orbit=77 radius=0.503761806492902 starId=-143401165 frame=false at=-65353,0,-118355 - body 3210136_-1258479_1657446 3210128_-1258480_1657458 kind=PLANET orbit=77 radius=1.0175020841222762 starId=-143401165 frame=true at=0,0,0 - body 3210136_-1258479_1657446 3210136_-1258479_1657446 kind=STAR orbit=0 radius=0.0 starId=-143401165 frame=true at=0,0,0 - body 3210136_-1258479_1657446 3210136_-1258479_1657447 kind=MOON orbit=7 radius=0.33538944125555403 starId=-143401165 frame=false at=149822,0,38139 - body 3210136_-1258479_1657446 3210136_-1258479_1657447 kind=PLANET orbit=7 radius=0.5614017489351886 starId=-143401165 frame=true at=0,0,0 - body 3210136_-1258479_1657446 3210142_-1258479_1657449 kind=PLANET orbit=35 radius=0.9817404084193058 starId=-143401165 frame=true at=0,0,0 - body 3210136_-1258479_1657446 3210151_-1258481_1657352 kind=ASTEROID_BELT orbit=507 radius=0.0 starId=-143401165 frame=true at=0,0,0 - body 3210136_-1258479_1657446 3210152_-1258477_1657503 kind=PLANET orbit=317 radius=1.924905001225997 starId=-143401165 frame=true at=0,0,0 - body 3405269_-1627426_6553174 3405269_-1627426_6553174 kind=ROGUE_PLANET orbit=0 radius=1.0902957609031467 starId=-1023282445 frame=true at=0,0,0 - body 3888408_1951506_402528 3866811_1951506_398707 kind=STAR orbit=117288 radius=94.20303580105305 starId=-1169505182 frame=true at=0,0,0 - body 3888408_1951506_402528 3888302_1951507_402590 kind=ASTEROID_BELT orbit=657 radius=0.0 starId=-1169505181 frame=true at=0,0,0 - body 3888408_1951506_402528 3888398_1951506_402530 kind=GAS_GIANT orbit=54 radius=8.213303587243715 starId=-1169505181 frame=true at=0,0,0 - body 3888408_1951506_402528 3888405_1951506_402529 kind=GAS_GIANT orbit=19 radius=7.863525818272932 starId=-1169505181 frame=true at=0,0,0 - body 3888408_1951506_402528 3888405_1951506_402529 kind=MOON orbit=19 radius=0.2028788302456146 starId=-1169505181 frame=false at=-389688,0,1835898 - body 3888408_1951506_402528 3888405_1951506_402529 kind=MOON orbit=19 radius=0.20609414861856426 starId=-1169505181 frame=false at=-1093080,0,-1854882 - body 3888408_1951506_402528 3888405_1951506_402529 kind=MOON orbit=19 radius=0.23354633994491927 starId=-1169505181 frame=false at=-1133377,0,-742252 - body 3888408_1951506_402528 3888405_1951506_402529 kind=MOON orbit=19 radius=0.5732224397545107 starId=-1169505181 frame=false at=650319,0,454150 - body 3888408_1951506_402528 3888407_1951506_402529 kind=ASTEROID_BELT orbit=10 radius=0.0 starId=-1169505181 frame=true at=0,0,0 - body 3888408_1951506_402528 3888408_1951506_402528 kind=STAR orbit=0 radius=0.0 starId=-1169505181 frame=true at=0,0,0 - body 3888408_1951506_402528 3888408_1951506_402530 kind=PLANET orbit=10 radius=0.4653093968013708 starId=-1169505181 frame=true at=0,0,0 - body 3888408_1951506_402528 3888409_1951506_402514 kind=GAS_GIANT orbit=74 radius=6.4434807244767 starId=-1169505181 frame=true at=0,0,0 - body 3888408_1951506_402528 3888409_1951506_402514 kind=MOON orbit=74 radius=0.27307165317760784 starId=-1169505181 frame=false at=-747621,0,1064981 - body 3888408_1951506_402528 3888409_1951506_402514 kind=MOON orbit=74 radius=0.3788747613111544 starId=-1169505181 frame=false at=645061,0,484252 - body 3888408_1951506_402528 3888409_1951506_402514 kind=MOON orbit=74 radius=0.4567167848973427 starId=-1169505181 frame=false at=433362,0,1066517 - body 3888408_1951506_402528 3888409_1951506_402514 kind=MOON orbit=74 radius=0.5702453561500446 starId=-1169505181 frame=false at=-430023,0,1012245 - body 3888408_1951506_402528 3888419_1951505_402484 kind=MOON orbit=240 radius=0.5460815191564856 starId=-1169505181 frame=false at=28983,0,-249522 - body 3888408_1951506_402528 3888419_1951505_402484 kind=MOON orbit=240 radius=0.7054016448066485 starId=-1169505181 frame=false at=-110458,0,-102956 - body 3888408_1951506_402528 3888419_1951505_402484 kind=PLANET orbit=240 radius=2.0003231974248514 starId=-1169505181 frame=true at=0,0,0 - body 3888408_1951506_402528 3888426_1951506_402453 kind=MOON orbit=411 radius=0.2080090024469129 starId=-1169505181 frame=false at=-515123,0,-354646 - body 3888408_1951506_402528 3888426_1951506_402453 kind=PLANET orbit=411 radius=2.481279125837847 starId=-1169505181 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4433045_-1299745_6671676 kind=STAR orbit=9636 radius=93.87084494948387 starId=-779017546 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434199_-1299751_6672916 kind=ASTEROID_BELT orbit=793 radius=0.0 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434341_-1299745_6672928 kind=PLANET orbit=48 radius=1.029447635674642 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434347_-1299745_6672921 kind=STAR orbit=0 radius=0.0 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434347_-1299745_6672926 kind=MOON orbit=29 radius=0.6746619882477115 starId=-779017545 frame=false at=1017,0,-35585 - body 4434347_-1299745_6672921 4434347_-1299745_6672926 kind=PLANET orbit=29 radius=0.5488513114065555 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434348_-1299745_6672921 kind=PLANET orbit=7 radius=1.1564058055441164 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434349_-1299745_6672922 kind=MOON orbit=13 radius=0.3128534811459255 starId=-779017545 frame=false at=49238,0,5979 - body 4434347_-1299745_6672921 4434349_-1299745_6672922 kind=MOON orbit=13 radius=0.4126258227463259 starId=-779017545 frame=false at=-134255,0,65999 - body 4434347_-1299745_6672921 4434349_-1299745_6672922 kind=PLANET orbit=13 radius=0.5263349713162284 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434355_-1299745_6672923 kind=ASTEROID_BELT orbit=42 radius=0.0 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=GAS_GIANT orbit=133 radius=7.415370376903965 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=MOON orbit=133 radius=0.21437447594308398 starId=-779017545 frame=false at=1616783,0,223806 - body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=MOON orbit=133 radius=0.40440708215634125 starId=-779017545 frame=false at=701850,0,-794359 - body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=MOON orbit=133 radius=0.6308604975479772 starId=-779017545 frame=false at=31306,0,1042130 - body 4434347_-1299745_6672921 4434356_-1299746_6672944 kind=MOON orbit=133 radius=0.6577279267763994 starId=-779017545 frame=false at=-606066,0,943991 - body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=GAS_GIANT orbit=77 radius=10.678467980226468 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=MOON orbit=77 radius=0.2661410111181117 starId=-779017545 frame=false at=-2886152,0,-482466 - body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=MOON orbit=77 radius=0.36269510031743357 starId=-779017545 frame=false at=-1794557,0,-1940364 - body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=MOON orbit=77 radius=0.36605921118550216 starId=-779017545 frame=false at=141975,0,-2155529 - body 4434347_-1299745_6672921 4434361_-1299745_6672921 kind=MOON orbit=77 radius=0.3744273970489842 starId=-779017545 frame=false at=2174829,0,1051764 - body 4434347_-1299745_6672921 4434396_-1299748_6672946 kind=GAS_GIANT orbit=295 radius=8.735151803026602 starId=-779017545 frame=true at=0,0,0 - body 4434347_-1299745_6672921 4434418_-1299749_6672981 kind=MOON orbit=496 radius=0.4409925348108476 starId=-779017545 frame=false at=346248,0,52464 - body 4434347_-1299745_6672921 4434418_-1299749_6672981 kind=PLANET orbit=496 radius=1.528871001395831 starId=-779017545 frame=true at=0,0,0 - body 4544946_-354196_1918720 4544946_-354196_1918720 kind=MOON orbit=0 radius=0.48546330791476977 starId=-916256941 frame=false at=48021,0,-19950 - body 4544946_-354196_1918720 4544946_-354196_1918720 kind=ROGUE_PLANET orbit=0 radius=0.6109303737088227 starId=-916256941 frame=true at=0,0,0 - body 5314096_-799494_-3236895 5314096_-799494_-3236895 kind=MOON orbit=0 radius=1.8164474484584918 starId=-640125417 frame=false at=125521,0,125077 - body 5314096_-799494_-3236895 5314096_-799494_-3236895 kind=MOON orbit=0 radius=1.877907646816893 starId=-640125417 frame=false at=-174768,0,101691 - body 5314096_-799494_-3236895 5314096_-799494_-3236895 kind=ROGUE_PLANET orbit=0 radius=0.7834351676919846 starId=-640125417 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400307_5307314_4625912 kind=MOON orbit=479 radius=0.36600502259152407 starId=-1596760973 frame=false at=185186,0,-188155 - body 5400348_5307314_4625833 5400307_5307314_4625912 kind=MOON orbit=479 radius=0.5353292044399672 starId=-1596760973 frame=false at=363113,0,292070 - body 5400348_5307314_4625833 5400307_5307314_4625912 kind=PLANET orbit=479 radius=2.0426135503873906 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400344_5307314_4625827 kind=PLANET orbit=40 radius=1.7687375272010322 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400344_5307314_4625828 kind=ASTEROID_BELT orbit=37 radius=0.0 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400344_5307314_4625834 kind=PLANET orbit=23 radius=1.8495286024364148 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400346_5307314_4625835 kind=MOON orbit=15 radius=0.5844551676721068 starId=-1596760973 frame=false at=4672,0,27405 - body 5400348_5307314_4625833 5400346_5307314_4625835 kind=PLANET orbit=15 radius=0.3309110169492018 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400348_5307314_4625833 kind=STAR orbit=0 radius=0.0 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400349_5307314_4625833 kind=MOON orbit=7 radius=0.5136895480255286 starId=-1596760973 frame=false at=-36569,0,-60157 - body 5400348_5307314_4625833 5400349_5307314_4625833 kind=MOON orbit=7 radius=0.6551993518714251 starId=-1596760973 frame=false at=19338,0,128351 - body 5400348_5307314_4625833 5400349_5307314_4625833 kind=PLANET orbit=7 radius=0.4631510958156348 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400357_5307313_4625779 kind=MOON orbit=292 radius=0.507106136396319 starId=-1596760973 frame=false at=-50023,0,-107314 - body 5400348_5307314_4625833 5400357_5307313_4625779 kind=MOON orbit=292 radius=0.5178855145286163 starId=-1596760973 frame=false at=28458,0,24267 - body 5400348_5307314_4625833 5400357_5307313_4625779 kind=PLANET orbit=292 radius=0.45405883853013485 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400359_5307314_4625839 kind=GAS_GIANT orbit=67 radius=5.964991020215376 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400368_5307314_4625842 kind=MOON orbit=119 radius=0.46433487866080986 starId=-1596760973 frame=false at=6216,0,-83770 - body 5400348_5307314_4625833 5400368_5307314_4625842 kind=PLANET orbit=119 radius=0.5261890870626688 starId=-1596760973 frame=true at=0,0,0 - body 5400348_5307314_4625833 5400486_5307316_4625794 kind=ASTEROID_BELT orbit=766 radius=0.0 starId=-1596760973 frame=true at=0,0,0 - body 5482981_6750208_3250872 5482944_6750208_3250841 kind=STAR orbit=257 radius=75.38633159816266 starId=-512586094 frame=true at=0,0,0 - body 5482981_6750208_3250872 5482961_6750209_3250857 kind=ASTEROID_BELT orbit=134 radius=0.0 starId=-512586093 frame=true at=0,0,0 - body 5482981_6750208_3250872 5482970_6750209_3250861 kind=PLANET orbit=84 radius=0.511386558342394 starId=-512586093 frame=true at=0,0,0 - body 5482981_6750208_3250872 5482979_6750208_3250872 kind=PLANET orbit=10 radius=2.0786570647083242 starId=-512586093 frame=true at=0,0,0 - body 5482981_6750208_3250872 5482981_6750208_3250872 kind=STAR orbit=0 radius=0.0 starId=-512586093 frame=true at=0,0,0 - body 5482981_6750208_3250872 5482982_6750208_3250876 kind=PLANET orbit=23 radius=0.3395689288691417 starId=-512586093 frame=true at=0,0,0 - body 569093_2111063_3969013 569093_2111063_3969013 kind=ROGUE_PLANET orbit=0 radius=0.6864916344948702 starId=-852908577 frame=true at=0,0,0 - body 5697881_301001_4979351 5697881_301001_4979351 kind=MOON orbit=0 radius=2.14719261106477 starId=-945380041 frame=false at=152014,0,122135 - body 5697881_301001_4979351 5697881_301001_4979351 kind=ROGUE_PLANET orbit=0 radius=0.6458161630069941 starId=-945380041 frame=true at=0,0,0 - body 6696574_1162321_-1072222 6696574_1162321_-1072222 kind=ROGUE_PLANET orbit=0 radius=1.2078455531653214 starId=-636290765 frame=true at=0,0,0 - body 6807485_4657985_-295159 6807485_4657985_-295159 kind=MOON orbit=0 radius=0.6561439936700475 starId=-862242349 frame=false at=31412,0,331113 - body 6807485_4657985_-295159 6807485_4657985_-295159 kind=MOON orbit=0 radius=0.7067552937739463 starId=-862242349 frame=false at=-107171,0,-266239 - body 6807485_4657985_-295159 6807485_4657985_-295159 kind=ROGUE_PLANET orbit=0 radius=1.1260629550442611 starId=-862242349 frame=true at=0,0,0 - derived -1230314_1893274_-2189019 -1230314_1893274_-2189019 type=barren mass=0.01555362067257163 radius=0.3271755891768876 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=71697 metallicity=1.2624377715010906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1735123_-1297562_-3230849 -1735123_-1297562_-3230849 type=ice mass=0.8667932843771754 radius=0.9122063191004484 gravity=104 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=21616 metallicity=0.5277737560293576 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1839127_-930469_3024425 -1839127_-930469_3024425 type=ice mass=2.4824153191183425 radius=1.2352684367448383 gravity=163 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=15461 metallicity=0.4548654886227668 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2967914_5695077_-2605367 type=gasgiant mass=66.67616439538739 radius=5.5771406010519655 gravity=214 pressure=1600 tempK=70 oxygen=false locked=false rings=true rotation=5084 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968046_5695074_-2605415 type=ice mass=1.2526682385003438 radius=1.0703624393599125 gravity=109 pressure=1600 tempK=127 oxygen=false locked=false rings=false rotation=14288 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968054_5695071_-2605430 type=ice mass=1.5204363781222705 radius=1.1112601957037147 gravity=123 pressure=1600 tempK=159 oxygen=false locked=false rings=false rotation=32177 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968066_5695072_-2605456 type=gasgiant mass=60.09856952315779 radius=5.330894440915554 gravity=211 pressure=1600 tempK=322 oxygen=false locked=false rings=false rotation=10205 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968066_5695072_-2605466 type=icegiant mass=52.653240129683724 radius=5.032996057500449 gravity=208 pressure=1600 tempK=239 oxygen=false locked=false rings=false rotation=10756 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968073_5695072_-2605452 type=greenhouse mass=26.85856394766772 radius=2.3001142453388033 gravity=400 pressure=1600 tempK=594 oxygen=false locked=true rings=false rotation=54735 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968074_5695072_-2605453 type=lava mass=1.3553886823435224 radius=1.1461775365084355 gravity=103 pressure=5 tempK=933 oxygen=false locked=true rings=false rotation=29885 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968075_5695072_-2605459 type=barren mass=0.10017701075525987 radius=0.5085798172977589 gravity=39 pressure=4 tempK=195 oxygen=false locked=true rings=false rotation=76477 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968080_5695072_-2605444 type=exotic mass=1.8070992624622861 radius=1.2180207924065842 gravity=122 pressure=1600 tempK=308 oxygen=false locked=false rings=false rotation=25228 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968141_5695075_-2605545 type=gasgiant mass=96.83774537928785 radius=6.559618412306246 gravity=225 pressure=1600 tempK=87 oxygen=false locked=false rings=true rotation=10130 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2968074_5695072_-2605453 -2968198_5695072_-2605067 type=gasgiant mass=213.1692061439288 radius=9.244209790920415 gravity=249 pressure=1600 tempK=48 oxygen=false locked=false rings=true rotation=11019 metallicity=0.9315878868567 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3037636_-286450_6061438 -3037636_-286450_6061438 type=ice mass=1.5263246991696868 radius=1.1321185551403936 gravity=119 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=26481 metallicity=0.6199677203655225 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3258554_1675500_3803298 -3258547_1675497_3803188 type=superearth mass=5.52311599764172 radius=1.6254072360065355 gravity=209 pressure=1600 tempK=83 oxygen=false locked=false rings=false rotation=39819 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3258554_1675500_3803298 -3258547_1675499_3803284 type=gasgiant mass=69.3043952234646 radius=5.6716793111039 gravity=215 pressure=1600 tempK=203 oxygen=false locked=false rings=true rotation=9067 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3258554_1675500_3803298 -3258554_1675500_3803298 type=barren mass=0.015484631073251742 radius=0.3089612503713161 gravity=16 pressure=0 tempK=954 oxygen=false locked=true rings=false rotation=30712 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3258554_1675500_3803298 -3258555_1675500_3803299 type=greenhouse mass=1.4526159580169788 radius=1.0933516689678018 gravity=122 pressure=305 tempK=366 oxygen=false locked=true rings=false rotation=42913 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3258554_1675500_3803298 -3258562_1675500_3803301 type=superearth mass=5.4324890429845 radius=1.5871017477083504 gravity=216 pressure=1600 tempK=299 oxygen=false locked=false rings=false rotation=10021 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3258554_1675500_3803298 -3258563_1675497_3803230 type=barren mass=0.004716973573928933 radius=0.22596366099897897 gravity=9 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=6688 metallicity=0.45806642349747906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -545630_6755069_2561467 -545471_6755071_2561465 type=gasgiant mass=261.670585699835 radius=10.105985698063467 gravity=256 pressure=1600 tempK=119 oxygen=false locked=false rings=true rotation=7464 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -545630_6755069_2561467 -545611_6755062_2561721 type=ice mass=2.7508147708605315 radius=1.28776136557378 gravity=166 pressure=1600 tempK=89 oxygen=false locked=false rings=false rotation=16466 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -545630_6755069_2561467 -545624_6755069_2561468 type=greenhouse mass=1.051255092694872 radius=1.055941406990247 gravity=94 pressure=187 tempK=290 oxygen=false locked=true rings=false rotation=20156 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -545630_6755069_2561467 -545630_6755069_2561467 type=lava mass=0.3707594162294638 radius=0.8080341280862053 gravity=57 pressure=0 tempK=1751 oxygen=false locked=true rings=false rotation=47258 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -545630_6755069_2561467 -545634_6755067_2561379 type=icegiant mass=37.722267124329875 radius=4.353696915708663 gravity=199 pressure=1600 tempK=159 oxygen=false locked=false rings=false rotation=6909 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -545630_6755069_2561467 -545646_6755070_2561489 type=barren mass=0.009702196688479214 radius=0.2807380026340163 gravity=12 pressure=0 tempK=148 oxygen=false locked=false rings=false rotation=47061 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -545630_6755069_2561467 -545654_6755068_2561424 type=icegiant mass=106.5408516143004 radius=6.837693990559999 gravity=228 pressure=1600 tempK=214 oxygen=false locked=false rings=true rotation=5294 metallicity=0.7052174814061872 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -755250_5247165_5302015 -755250_5247165_5302015 type=ice mass=4.274166668479209 radius=1.5423697863264323 gravity=180 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=73337 metallicity=0.437474442147087 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -758984_1485681_3107299 -758984_1485681_3107299 type=superearth mass=33.05924302142529 radius=2.484840931637559 gravity=400 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=23269 metallicity=0.8875026328136386 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1159501_438377_2970042 1159501_438377_2970042 type=barren mass=0.0045062770008992325 radius=0.2438710079048173 gravity=8 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=24830 metallicity=0.4161491356171123 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1502575_5241619_578848 1502575_5241619_578848 type=ice mass=24.038493540644538 radius=2.2738202904677567 gravity=400 pressure=0 tempK=51 oxygen=false locked=false rings=false rotation=11883 metallicity=0.44569587581936077 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1915131_1673389_-1943074 1915131_1673389_-1943074 type=barren mass=0.08478477173619915 radius=0.5320514501890937 gravity=30 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=11849 metallicity=1.0785905850404696 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2639269_-1847617_-2599924 2639190_-1847611_-2600049 type=barren mass=0.017027347609292 radius=0.3444434132626236 gravity=14 pressure=2 tempK=61 oxygen=false locked=false rings=false rotation=11720 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2639269_-1847617_-2599924 2639267_-1847617_-2599925 type=desert mass=0.012680252203835457 radius=0.29987989003936566 gravity=14 pressure=0 tempK=275 oxygen=false locked=true rings=false rotation=11976 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2639269_-1847617_-2599924 2639269_-1847617_-2599924 type=barren mass=0.03122808828533539 radius=0.38794364457623254 gravity=21 pressure=0 tempK=954 oxygen=false locked=true rings=false rotation=46664 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2639269_-1847617_-2599924 2639272_-1847617_-2599928 type=barren mass=0.029935375461684652 radius=0.369897875724714 gravity=22 pressure=1 tempK=195 oxygen=false locked=true rings=false rotation=82646 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2639269_-1847617_-2599924 2639274_-1847617_-2599896 type=barren mass=0.045072349982484945 radius=0.42914253222295917 gravity=24 pressure=2 tempK=122 oxygen=false locked=false rings=false rotation=9301 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2639269_-1847617_-2599924 2639288_-1847617_-2599929 type=gasgiant mass=143.70130978715503 radius=7.78766563501808 gravity=237 pressure=1600 tempK=265 oxygen=false locked=false rings=false rotation=8848 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2639269_-1847617_-2599924 2639505_-1847606_-2599928 type=ice mass=25.37309969822193 radius=2.311518149114087 gravity=400 pressure=1600 tempK=90 oxygen=false locked=false rings=false rotation=14436 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2639269_-1847617_-2599924 2641577_-1847617_-2605840 type=ice mass=0.23217555047715116 radius=0.6570569261622582 gravity=54 pressure=474 tempK=13 oxygen=false locked=false rings=false rotation=20919 metallicity=1.255824071958323 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2646744_5580083_-2997664 2646704_5580086_-2997730 type=ice mass=10.179842847683847 radius=1.792136039338186 gravity=317 pressure=1600 tempK=86 oxygen=false locked=false rings=false rotation=15461 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2646744_5580083_-2997664 2646744_5580083_-2997664 type=lava mass=25.33317144479418 radius=2.409159572567257 gravity=400 pressure=1600 tempK=2155 oxygen=false locked=true rings=false rotation=23474 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2646744_5580083_-2997664 2646746_5580083_-2997663 type=greenhouse mass=19.54000702893978 radius=2.3360233067337304 gravity=358 pressure=1600 tempK=433 oxygen=false locked=true rings=false rotation=8760 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2646744_5580083_-2997664 2646748_5580083_-2997658 type=superearth mass=6.889121985182743 radius=1.7389342248982458 gravity=228 pressure=1600 tempK=324 oxygen=false locked=true rings=false rotation=69188 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2646744_5580083_-2997664 2646762_5580082_-2997709 type=ice mass=0.0035763601522213286 radius=0.2127699482289579 gravity=8 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=22025 metallicity=0.5618254474018163 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2868006_4842033_4291324 2868006_4842033_4291324 type=ice mass=9.026670198345228 radius=1.7074633543397322 gravity=310 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=39771 metallicity=0.7832019177087589 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3210136_-1258479_1657446 3210128_-1258480_1657458 type=exotic mass=1.0668384556722796 radius=1.0175020841222762 gravity=103 pressure=1600 tempK=231 oxygen=false locked=false rings=false rotation=7932 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3210136_-1258479_1657446 3210136_-1258479_1657446 type=lava mass=0.14034585335322858 radius=0.5565614447937366 gravity=45 pressure=0 tempK=962 oxygen=false locked=true rings=false rotation=13092 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3210136_-1258479_1657446 3210136_-1258479_1657447 type=barren mass=0.1447581242742227 radius=0.5614017489351886 gravity=46 pressure=4 tempK=361 oxygen=false locked=true rings=false rotation=13189 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3210136_-1258479_1657446 3210142_-1258479_1657449 type=ocean mass=1.0852870404305224 radius=0.9817404084193058 gravity=113 pressure=390 tempK=257 oxygen=true locked=true rings=false rotation=19343 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3210136_-1258479_1657446 3210151_-1258481_1657352 type=gasgiant mass=23.98213586106359 radius=3.575461754416799 gravity=188 pressure=1600 tempK=83 oxygen=false locked=false rings=false rotation=10211 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3210136_-1258479_1657446 3210152_-1258477_1657503 type=superearth mass=10.039623894828077 radius=1.924905001225997 gravity=271 pressure=1600 tempK=114 oxygen=false locked=false rings=false rotation=17243 metallicity=0.8744918612953547 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3405269_-1627426_6553174 3405269_-1627426_6553174 type=ice mass=1.665831257268868 radius=1.0902957609031467 gravity=140 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=93192 metallicity=0.5227600746932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3866811_1951506_398707 type=ice mass=1.1759586814894833 radius=1.095697036970876 gravity=98 pressure=1600 tempK=6 oxygen=false locked=false rings=false rotation=75800 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3888302_1951507_402590 type=ice mass=0.586717705320025 radius=0.8164094286328627 gravity=88 pressure=1600 tempK=76 oxygen=false locked=false rings=false rotation=10881 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3888398_1951506_402530 type=gasgiant mass=162.41085724528673 radius=8.213303587243715 gravity=241 pressure=1600 tempK=281 oxygen=false locked=false rings=false rotation=14245 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3888405_1951506_402529 type=gasgiant mass=146.94126466276754 radius=7.863525818272932 gravity=238 pressure=1600 tempK=475 oxygen=false locked=false rings=true rotation=12031 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3888407_1951506_402529 type=desert mass=0.13526828875544214 radius=0.6096025245184175 gravity=36 pressure=2 tempK=316 oxygen=false locked=true rings=false rotation=8558 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3888408_1951506_402528 type=barren mass=0.044849501740026304 radius=0.45681884206073997 gravity=21 pressure=0 tempK=1060 oxygen=false locked=true rings=false rotation=22039 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3888408_1951506_402530 type=barren mass=0.04785511737114254 radius=0.4653093968013708 gravity=22 pressure=0 tempK=335 oxygen=false locked=true rings=false rotation=21716 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3888409_1951506_402514 type=gasgiant mass=92.93967973090871 radius=6.4434807244767 gravity=224 pressure=1600 tempK=240 oxygen=false locked=false rings=true rotation=9219 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3888419_1951505_402484 type=ice mass=13.869969155191379 radius=2.0003231974248514 gravity=347 pressure=1600 tempK=126 oxygen=false locked=false rings=false rotation=6262 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3888408_1951506_402528 3888426_1951506_402453 type=ice mass=24.646343623131614 radius=2.481279125837847 gravity=400 pressure=1600 tempK=96 oxygen=false locked=false rings=false rotation=14106 metallicity=1.3225919278995582 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4433045_-1299745_6671676 type=icegiant mass=274.59198337216975 radius=10.320006262012992 gravity=258 pressure=1600 tempK=22 oxygen=false locked=false rings=true rotation=5405 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434199_-1299751_6672916 type=ice mass=0.005088301601998299 radius=0.24188463795701431 gravity=9 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=20536 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434341_-1299745_6672928 type=exotic mass=1.1636338753392348 radius=1.029447635674642 gravity=110 pressure=1600 tempK=316 oxygen=false locked=true rings=false rotation=24767 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434347_-1299745_6672921 type=barren mass=0.0943230398749093 radius=0.5440812193502986 gravity=32 pressure=0 tempK=1033 oxygen=false locked=true rings=false rotation=6419 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434347_-1299745_6672926 type=barren mass=0.0966967689506941 radius=0.5488513114065555 gravity=32 pressure=2 tempK=191 oxygen=false locked=true rings=false rotation=6096 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434348_-1299745_6672921 type=exotic mass=2.0450559378617754 radius=1.1564058055441164 gravity=153 pressure=90 tempK=404 oxygen=false locked=true rings=false rotation=43546 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434349_-1299745_6672922 type=barren mass=0.1021600990316428 radius=0.5263349713162284 gravity=37 pressure=1 tempK=286 oxygen=false locked=true rings=false rotation=26236 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434355_-1299745_6672923 type=ice mass=0.004157776232024905 radius=0.24161793660069364 gravity=7 pressure=0 tempK=130 oxygen=false locked=true rings=false rotation=33046 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434356_-1299746_6672944 type=gasgiant mass=128.38949594196157 radius=7.415370376903965 gravity=233 pressure=1600 tempK=175 oxygen=false locked=false rings=false rotation=10714 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434361_-1299745_6672921 type=icegiant mass=297.0260422938207 radius=10.678467980226468 gravity=260 pressure=1600 tempK=230 oxygen=false locked=false rings=false rotation=13470 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434396_-1299748_6672946 type=gasgiant mass=187.13111039273838 radius=8.735151803026602 gravity=245 pressure=1600 tempK=117 oxygen=false locked=false rings=false rotation=13321 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4434347_-1299745_6672921 4434418_-1299749_6672981 type=superearth mass=5.744501687852009 radius=1.528871001395831 gravity=246 pressure=1600 tempK=98 oxygen=false locked=false rings=false rotation=10726 metallicity=1.026549818051544 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4544946_-354196_1918720 4544946_-354196_1918720 type=barren mass=0.19972857631014948 radius=0.6109303737088227 gravity=54 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=19942 metallicity=0.7583475794397297 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5314096_-799494_-3236895 5314096_-799494_-3236895 type=barren mass=0.4590735898881811 radius=0.7834351676919846 gravity=75 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=38621 metallicity=1.405033353237831 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400307_5307314_4625912 type=superearth mass=13.341372461812089 radius=2.0426135503873906 gravity=320 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=12137 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400344_5307314_4625827 type=superearth mass=8.932627278838716 radius=1.7687375272010322 gravity=286 pressure=1600 tempK=331 oxygen=false locked=true rings=false rotation=8346 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400344_5307314_4625828 type=superearth mass=10.098916569236577 radius=1.8314307830807581 gravity=301 pressure=1600 tempK=344 oxygen=false locked=true rings=false rotation=8044 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400344_5307314_4625834 type=greenhouse mass=10.447232336444829 radius=1.8495286024364148 gravity=305 pressure=1600 tempK=338 oxygen=false locked=true rings=false rotation=8919 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400346_5307314_4625835 type=barren mass=0.01609865061852627 radius=0.3309110169492018 gravity=15 pressure=0 tempK=254 oxygen=false locked=true rings=false rotation=9851 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400348_5307314_4625833 type=lava mass=7.099280932494793 radius=1.7755434460175963 gravity=225 pressure=229 tempK=1373 oxygen=false locked=true rings=false rotation=40861 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400349_5307314_4625833 type=barren mass=0.050951093437401285 radius=0.4631510958156348 gravity=24 pressure=0 tempK=373 oxygen=false locked=true rings=false rotation=11012 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400357_5307313_4625779 type=ice mass=0.055267288021912254 radius=0.45405883853013485 gravity=27 pressure=53 tempK=47 oxygen=false locked=false rings=false rotation=18799 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400359_5307314_4625839 type=gasgiant mass=77.82631559060034 radius=5.964991020215376 gravity=219 pressure=1600 tempK=235 oxygen=false locked=false rings=true rotation=7819 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400368_5307314_4625842 type=barren mass=0.07762502485035572 radius=0.5261890870626688 gravity=28 pressure=9 tempK=90 oxygen=false locked=false rings=false rotation=89423 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5400348_5307314_4625833 5400486_5307316_4625794 type=ice mass=0.09754862785267554 radius=0.5276029785816756 gravity=35 pressure=125 tempK=34 oxygen=false locked=false rings=false rotation=90122 metallicity=1.1307121381430347 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5482981_6750208_3250872 5482944_6750208_3250841 type=ice mass=9.876000388503094 radius=1.830097901279705 gravity=295 pressure=1600 tempK=131 oxygen=false locked=false rings=false rotation=16661 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5482981_6750208_3250872 5482961_6750209_3250857 type=superearth mass=17.871248045341385 radius=2.0729948379254326 gravity=400 pressure=1600 tempK=202 oxygen=false locked=false rings=false rotation=81929 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5482981_6750208_3250872 5482970_6750209_3250861 type=ice mass=0.0640682306634592 radius=0.511386558342394 gravity=24 pressure=7 tempK=97 oxygen=false locked=false rings=true rotation=30074 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5482981_6750208_3250872 5482979_6750208_3250872 type=superearth mass=14.867434224085532 radius=2.0786570647083242 gravity=344 pressure=1600 tempK=724 oxygen=false locked=true rings=false rotation=21206 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5482981_6750208_3250872 5482981_6750208_3250872 type=lava mass=5.898210294400681 radius=1.684516598207336 gravity=208 pressure=189 tempK=1429 oxygen=false locked=true rings=false rotation=58676 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5482981_6750208_3250872 5482982_6750208_3250876 type=barren mass=0.01757119920794134 radius=0.3395689288691417 gravity=15 pressure=0 tempK=224 oxygen=false locked=true rings=false rotation=50463 metallicity=1.5818859163232135 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 569093_2111063_3969013 569093_2111063_3969013 type=ice mass=0.23275880995672615 radius=0.6864916344948702 gravity=49 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=12095 metallicity=1.2057393145105653 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5697881_301001_4979351 5697881_301001_4979351 type=barren mass=0.23499421065383816 radius=0.6458161630069941 gravity=56 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=12504 metallicity=0.7927308232109274 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6696574_1162321_-1072222 6696574_1162321_-1072222 type=ice mass=1.9750536926360533 radius=1.2078455531653214 gravity=135 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=8748 metallicity=0.6735222436120273 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6807485_4657985_-295159 6807485_4657985_-295159 type=ice mass=1.2829558170167987 radius=1.1260629550442611 gravity=101 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=33919 metallicity=1.0404619585816652 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1230314_1893274_-2189019 id=-1485753477 kind=ROGUE_PLANET name=PGR--3525313.0.-3525313 starless - system -1735123_-1297562_-3230849 id=-457888649 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless - system -1839127_-930469_3024425 id=-1123773025 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless - system -2968074_5695072_-2605453 id=-423794625 kind=STAR name=PGS--3525313.3525313.-3525313 starTemp=40 starSize=0.7281860113143921 - system -3037636_-286450_6061438 id=-772485517 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless - system -3258554_1675500_3803298 id=-472927549 kind=STAR name=PGS--3525313.0.3525313 starTemp=40 starSize=0.7763141989707947 - system -545630_6755069_2561467 id=-1674485141 kind=STAR name=PGS--3525313.3525313.0 starTemp=70 starSize=0.8436675667762756 - system -755250_5247165_5302015 id=-157245369 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless - system -758984_1485681_3107299 id=-726144233 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless - system 1159501_438377_2970042 id=-875095761 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 1502575_5241619_578848 id=-440778097 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless - system 1915131_1673389_-1943074 id=-268045669 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless - system 2639269_-1847617_-2599924 id=-1586685861 kind=STAR name=PGS-0.-3525313.-3525313 starTemp=40 starSize=0.7759418487548828 - system 2646744_5580083_-2997664 id=-1182950741 kind=STAR name=PGS-0.3525313.-3525313 starTemp=40 starSize=0.7727809548377991 - system 2868006_4842033_4291324 id=-1484690657 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless - system 3210136_-1258479_1657446 id=-143401165 kind=STAR name=PGS-0.-3525313.0 starTemp=40 starSize=0.7805290818214417 - system 3405269_-1627426_6553174 id=-1023282445 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless - system 3888408_1951506_402528 id=-1169505181 kind=STAR name=PGS-3525313.0.0 starTemp=40 starSize=0.9578028321266174 - system 4434347_-1299745_6672921 id=-779017545 kind=STAR name=PGS-3525313.-3525313.3525313 starTemp=40 starSize=0.9092349410057068 - system 4544946_-354196_1918720 id=-916256941 kind=ROGUE_PLANET name=PGR-3525313.-3525313.0 starless - system 5314096_-799494_-3236895 id=-640125417 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless - system 5400348_5307314_4625833 id=-1596760973 kind=STAR name=PGS-3525313.3525313.3525313 starTemp=40 starSize=0.8300331234931946 - system 5482981_6750208_3250872 id=-512586093 kind=STAR name=PGS-3525313.3525313.0 starTemp=40 starSize=0.9896072149276733 - system 569093_2111063_3969013 id=-852908577 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless - system 5697881_301001_4979351 id=-945380041 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless - system 6696574_1162321_-1072222 id=-636290765 kind=ROGUE_PLANET name=PGR-3525313.0.-3525313 starless - system 6807485_4657985_-295159 id=-862242349 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless -seed 42 systems=27 - body -1093770_2928517_-2936619 -1093710_2928517_-2936631 kind=MOON orbit=330 radius=0.2349662187411855 starId=-953287813 frame=false at=176948,0,-210510 - body -1093770_2928517_-2936619 -1093710_2928517_-2936631 kind=MOON orbit=330 radius=0.2736195914688149 starId=-953287813 frame=false at=-18793,0,-189671 - body -1093770_2928517_-2936619 -1093710_2928517_-2936631 kind=PLANET orbit=330 radius=1.0640082304214584 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093714_2928520_-2936554 kind=MOON orbit=458 radius=0.511620609578344 starId=-953287813 frame=false at=-79088,0,33268 - body -1093770_2928517_-2936619 -1093714_2928520_-2936554 kind=PLANET orbit=458 radius=0.7478926338807454 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093748_2928518_-2936619 kind=GAS_GIANT orbit=119 radius=10.193231645391162 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093750_2928505_-2936328 kind=PLANET orbit=1562 radius=0.20027460427854427 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093763_2928518_-2936631 kind=MOON orbit=73 radius=0.25298812765572387 starId=-953287813 frame=false at=133586,0,65998 - body -1093770_2928517_-2936619 -1093763_2928518_-2936631 kind=MOON orbit=73 radius=0.6787510506387318 starId=-953287813 frame=false at=280077,0,65349 - body -1093770_2928517_-2936619 -1093763_2928518_-2936631 kind=PLANET orbit=73 radius=1.1267386015753886 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093768_2928515_-2936470 kind=GAS_GIANT orbit=798 radius=10.529056608271485 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093768_2928515_-2936470 kind=MOON orbit=798 radius=0.2746479955738943 starId=-953287813 frame=false at=-1529796,0,-1576874 - body -1093770_2928517_-2936619 -1093769_2928517_-2936617 kind=STAR orbit=13 radius=69.80905731260776 starId=-953287814 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093770_2928517_-2936619 kind=STAR orbit=0 radius=0.0 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093771_2928517_-2936631 kind=PLANET orbit=62 radius=0.2851886889287729 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093778_2928517_-2936610 kind=ASTEROID_BELT orbit=66 radius=0.0 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093791_2928516_-2936641 kind=GAS_GIANT orbit=162 radius=3.3296796457692377 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093791_2928516_-2936641 kind=MOON orbit=162 radius=0.22354478862853186 starId=-953287813 frame=false at=491318,0,-871898 - body -1093770_2928517_-2936619 -1093791_2928516_-2936641 kind=MOON orbit=162 radius=0.26443852401660384 starId=-953287813 frame=false at=-704343,0,-287076 - body -1093770_2928517_-2936619 -1093811_2928515_-2936618 kind=MOON orbit=220 radius=0.5192411316972985 starId=-953287813 frame=false at=98567,0,67031 - body -1093770_2928517_-2936619 -1093811_2928515_-2936618 kind=MOON orbit=220 radius=0.7261851786087157 starId=-953287813 frame=false at=52964,0,91588 - body -1093770_2928517_-2936619 -1093811_2928515_-2936618 kind=PLANET orbit=220 radius=0.41868787916123795 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093828_2928521_-2936524 kind=GAS_GIANT orbit=597 radius=6.967877626193331 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1093967_2928517_-2936558 kind=GAS_GIANT orbit=1103 radius=5.229190104169739 starId=-953287813 frame=true at=0,0,0 - body -1093770_2928517_-2936619 -1094040_2928511_-2937000 kind=ASTEROID_BELT orbit=2499 radius=0.0 starId=-953287813 frame=true at=0,0,0 - body -1332546_1631083_3243750 -1332546_1631083_3243750 kind=MOON orbit=0 radius=0.4135480649211427 starId=-1436132233 frame=false at=41673,0,19000 - body -1332546_1631083_3243750 -1332546_1631083_3243750 kind=MOON orbit=0 radius=2.2855349913038356 starId=-1436132233 frame=false at=51650,0,-27681 - body -1332546_1631083_3243750 -1332546_1631083_3243750 kind=ROGUE_PLANET orbit=0 radius=0.20027007926536833 starId=-1436132233 frame=true at=0,0,0 - body -1502067_-1460437_4865055 -1502067_-1460437_4865055 kind=MOON orbit=0 radius=2.232997121416223 starId=-1542560749 frame=false at=-110869,0,-184209 - body -1502067_-1460437_4865055 -1502067_-1460437_4865055 kind=ROGUE_PLANET orbit=0 radius=0.8722168368675713 starId=-1542560749 frame=true at=0,0,0 - body -2327343_6217060_-3227882 -2327248_6217066_-3227948 kind=ASTEROID_BELT orbit=622 radius=0.0 starId=-638227073 frame=true at=0,0,0 - body -2327343_6217060_-3227882 -2327331_6217060_-3227869 kind=GAS_GIANT orbit=96 radius=6.206626820771696 starId=-638227073 frame=true at=0,0,0 - body -2327343_6217060_-3227882 -2327343_6217060_-3227882 kind=STAR orbit=0 radius=0.0 starId=-638227073 frame=true at=0,0,0 - body -2327343_6217060_-3227882 -2327344_6217060_-3227874 kind=PLANET orbit=41 radius=0.617305205704887 starId=-638227073 frame=true at=0,0,0 - body -2327343_6217060_-3227882 -2327344_6217060_-3227879 kind=PLANET orbit=15 radius=0.6433805032761719 starId=-638227073 frame=true at=0,0,0 - body -2327343_6217060_-3227882 -2327353_6217060_-3227880 kind=ASTEROID_BELT orbit=53 radius=0.0 starId=-638227073 frame=true at=0,0,0 - body -2327343_6217060_-3227882 -2327394_6217059_-3227934 kind=MOON orbit=389 radius=0.26035692394745363 starId=-638227073 frame=false at=78722,0,18208 - body -2327343_6217060_-3227882 -2327394_6217059_-3227934 kind=PLANET orbit=389 radius=0.263927013854739 starId=-638227073 frame=true at=0,0,0 - body -3264137_6862129_5633995 -3264137_6862129_5633995 kind=ROGUE_PLANET orbit=0 radius=1.1459793325492187 starId=-1878334477 frame=true at=0,0,0 - body -3272771_-2281626_-427326 -3272606_-2281621_-427320 kind=ASTEROID_BELT orbit=884 radius=0.0 starId=-1787505529 frame=true at=0,0,0 - body -3272771_-2281626_-427326 -3272685_-2281627_-427268 kind=GAS_GIANT orbit=553 radius=4.924089526202804 starId=-1787505529 frame=true at=0,0,0 - body -3272771_-2281626_-427326 -3272728_-2281624_-427344 kind=PLANET orbit=248 radius=0.48115646573370274 starId=-1787505529 frame=true at=0,0,0 - body -3272771_-2281626_-427326 -3272756_-2281626_-427334 kind=PLANET orbit=89 radius=1.6998160251693322 starId=-1787505529 frame=true at=0,0,0 - body -3272771_-2281626_-427326 -3272770_-2281626_-427322 kind=PLANET orbit=24 radius=1.1261802381726516 starId=-1787505529 frame=true at=0,0,0 - body -3272771_-2281626_-427326 -3272770_-2281626_-427327 kind=PLANET orbit=7 radius=1.0766820549511433 starId=-1787505529 frame=true at=0,0,0 - body -3272771_-2281626_-427326 -3272771_-2281626_-427326 kind=STAR orbit=0 radius=0.0 starId=-1787505529 frame=true at=0,0,0 - body -3272771_-2281626_-427326 -3272775_-2281626_-427332 kind=PLANET orbit=37 radius=0.4006648591407689 starId=-1787505529 frame=true at=0,0,0 - body -3272771_-2281626_-427326 -3272786_-2281624_-427271 kind=ASTEROID_BELT orbit=307 radius=0.0 starId=-1787505529 frame=true at=0,0,0 - body -3382269_4609397_102289 -3382162_4609391_102429 kind=ASTEROID_BELT orbit=945 radius=0.0 starId=-1804580365 frame=true at=0,0,0 - body -3382269_4609397_102289 -3382258_4609397_102291 kind=PLANET orbit=62 radius=1.1945742911747057 starId=-1804580365 frame=true at=0,0,0 - body -3382269_4609397_102289 -3382269_4609397_102289 kind=STAR orbit=0 radius=0.0 starId=-1804580365 frame=true at=0,0,0 - body -3382269_4609397_102289 -3382271_4609397_102293 kind=PLANET orbit=24 radius=2.4553590705256463 starId=-1804580365 frame=true at=0,0,0 - body -3382269_4609397_102289 -3382275_4609398_102253 kind=PLANET orbit=193 radius=0.32843835951638584 starId=-1804580365 frame=true at=0,0,0 - body -3382269_4609397_102289 -3382347_4609393_102367 kind=MOON orbit=591 radius=0.22586605779187946 starId=-1804580365 frame=false at=-414275,0,-295043 - body -3382269_4609397_102289 -3382347_4609393_102367 kind=MOON orbit=591 radius=0.23037156673038364 starId=-1804580365 frame=false at=122866,0,-24063 - body -3382269_4609397_102289 -3382347_4609393_102367 kind=PLANET orbit=591 radius=1.7294190750209564 starId=-1804580365 frame=true at=0,0,0 - body -3382269_4609397_102289 -3382624_4609397_102449 kind=STAR orbit=2080 radius=72.31004428625107 starId=-1804580366 frame=true at=0,0,0 - body -446119_-2387600_1528743 -430224_-2387600_1538055 kind=STAR orbit=98513 radius=70.60047593951225 starId=-1238572758 frame=true at=0,0,0 - body -446119_-2387600_1528743 -446071_-2387599_1528765 kind=MOON orbit=285 radius=0.24309845196571753 starId=-1238572757 frame=false at=296699,0,-7759 - body -446119_-2387600_1528743 -446071_-2387599_1528765 kind=PLANET orbit=285 radius=1.8371459761843838 starId=-1238572757 frame=true at=0,0,0 - body -446119_-2387600_1528743 -446073_-2387603_1528672 kind=ASTEROID_BELT orbit=456 radius=0.0 starId=-1238572757 frame=true at=0,0,0 - body -446119_-2387600_1528743 -446114_-2387600_1528734 kind=GAS_GIANT orbit=53 radius=7.324245297594027 starId=-1238572757 frame=true at=0,0,0 - body -446119_-2387600_1528743 -446114_-2387600_1528734 kind=MOON orbit=53 radius=0.2315350492962238 starId=-1238572757 frame=false at=706219,0,-929558 - body -446119_-2387600_1528743 -446114_-2387600_1528734 kind=MOON orbit=53 radius=0.38143339345610094 starId=-1238572757 frame=false at=-1782708,0,1014985 - body -446119_-2387600_1528743 -446114_-2387600_1528734 kind=MOON orbit=53 radius=0.5248470677369199 starId=-1238572757 frame=false at=126197,0,489391 - body -446119_-2387600_1528743 -446116_-2387600_1528747 kind=ASTEROID_BELT orbit=29 radius=0.0 starId=-1238572757 frame=true at=0,0,0 - body -446119_-2387600_1528743 -446118_-2387600_1528741 kind=MOON orbit=13 radius=0.20365004413625312 starId=-1238572757 frame=false at=-72832,0,-71410 - body -446119_-2387600_1528743 -446118_-2387600_1528741 kind=PLANET orbit=13 radius=1.3953997874664419 starId=-1238572757 frame=true at=0,0,0 - body -446119_-2387600_1528743 -446119_-2387600_1528743 kind=STAR orbit=0 radius=0.0 starId=-1238572757 frame=true at=0,0,0 - body -612264_1154834_5810641 -612264_1154834_5810641 kind=ROGUE_PLANET orbit=0 radius=0.4898994165169739 starId=-912475673 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1301912_-3086588_1331515 kind=ASTEROID_BELT orbit=3240 radius=0.0 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302043_-3086592_1332186 kind=GAS_GIANT orbit=662 radius=10.943876624718687 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302078_-3086584_1332016 kind=GAS_GIANT orbit=429 radius=3.7203469240327802 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302078_-3086584_1332016 kind=MOON orbit=429 radius=0.20017428524080313 starId=-993759433 frame=false at=-553114,0,-371691 - body 1302116_-3086586_1332086 1302078_-3086584_1332016 kind=MOON orbit=429 radius=0.38607883059392345 starId=-993759433 frame=false at=861833,0,-444261 - body 1302116_-3086586_1332086 1302107_-3086586_1332065 kind=GAS_GIANT orbit=121 radius=10.911882841329781 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302109_-3086586_1332079 kind=PLANET orbit=53 radius=0.20168145982592578 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302113_-3086586_1332071 kind=PLANET orbit=81 radius=0.3769303000187614 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302114_-3086586_1332089 kind=MOON orbit=20 radius=0.23277704186615006 starId=-993759433 frame=false at=512423,0,328354 - body 1302116_-3086586_1332086 1302114_-3086586_1332089 kind=MOON orbit=20 radius=0.4590817541896891 starId=-993759433 frame=false at=-59243,0,-288173 - body 1302116_-3086586_1332086 1302114_-3086586_1332089 kind=PLANET orbit=20 radius=2.0413133728416963 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302116_-3086586_1332086 kind=STAR orbit=0 radius=0.0 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302118_-3086586_1332087 kind=MOON orbit=13 radius=0.25425560347671766 starId=-993759433 frame=false at=50790,0,16491 - body 1302116_-3086586_1332086 1302118_-3086586_1332087 kind=MOON orbit=13 radius=0.3763175993207074 starId=-993759433 frame=false at=27886,0,-85992 - body 1302116_-3086586_1332086 1302118_-3086586_1332087 kind=PLANET orbit=13 radius=0.3079462488366804 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302121_-3086586_1332088 kind=PLANET orbit=29 radius=0.6327276283379721 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302129_-3086585_1332086 kind=ASTEROID_BELT orbit=67 radius=0.0 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302140_-3086586_1332065 kind=PLANET orbit=169 radius=2.3337921361694023 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302170_-3086588_1332078 kind=MOON orbit=294 radius=0.20052505369228113 starId=-993759433 frame=false at=-36866,0,7392 - body 1302116_-3086586_1332086 1302170_-3086588_1332078 kind=PLANET orbit=294 radius=0.36453462415151583 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302184_-3086594_1331905 kind=PLANET orbit=1032 radius=0.6157141695354549 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302488_-3086594_1332155 kind=GAS_GIANT orbit=2025 radius=10.002498125514519 starId=-993759433 frame=true at=0,0,0 - body 1302116_-3086586_1332086 1302488_-3086594_1332155 kind=MOON orbit=2025 radius=0.4638800680553467 starId=-993759433 frame=false at=806665,0,655155 - body 1302116_-3086586_1332086 1302488_-3086594_1332155 kind=MOON orbit=2025 radius=0.5150449302607414 starId=-993759433 frame=false at=-872558,0,997395 - body 1609421_-1918473_4755340 1609421_-1918473_4755340 kind=ROGUE_PLANET orbit=0 radius=0.7827230613996494 starId=-57202221 frame=true at=0,0,0 - body 1899532_3100584_4509498 1899470_3100581_4509619 kind=ASTEROID_BELT orbit=729 radius=0.0 starId=-810501533 frame=true at=0,0,0 - body 1899532_3100584_4509498 1899514_3100584_4509415 kind=PLANET orbit=456 radius=0.20516492708996092 starId=-810501533 frame=true at=0,0,0 - body 1899532_3100584_4509498 1899521_3100583_4509481 kind=MOON orbit=109 radius=0.3670423958318636 starId=-810501533 frame=false at=64062,0,-34158 - body 1899532_3100584_4509498 1899521_3100583_4509481 kind=PLANET orbit=109 radius=0.32640763764967407 starId=-810501533 frame=true at=0,0,0 - body 1899532_3100584_4509498 1899531_3100584_4509493 kind=PLANET orbit=25 radius=0.8240496319587478 starId=-810501533 frame=true at=0,0,0 - body 1899532_3100584_4509498 1899532_3100584_4509497 kind=PLANET orbit=7 radius=1.278950213656414 starId=-810501533 frame=true at=0,0,0 - body 1899532_3100584_4509498 1899532_3100584_4509498 kind=STAR orbit=0 radius=0.0 starId=-810501533 frame=true at=0,0,0 - body 2031833_5218371_1147521 2031833_5218371_1147521 kind=MOON orbit=0 radius=1.5534761570116726 starId=-770357401 frame=false at=-36421,0,249152 - body 2031833_5218371_1147521 2031833_5218371_1147521 kind=ROGUE_PLANET orbit=0 radius=1.2501648106704106 starId=-770357401 frame=true at=0,0,0 - body 2106667_5115784_6590371 2106667_5115784_6590371 kind=MOON orbit=0 radius=1.666309767693942 starId=-714236625 frame=false at=-226712,0,242266 - body 2106667_5115784_6590371 2106667_5115784_6590371 kind=ROGUE_PLANET orbit=0 radius=1.4995482876777844 starId=-714236625 frame=true at=0,0,0 - body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=GAS_GIANT orbit=21 radius=9.510681930029026 starId=-196901933 frame=true at=0,0,0 - body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=MOON orbit=21 radius=0.2091317529424837 starId=-196901933 frame=false at=-867790,0,-837770 - body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=MOON orbit=21 radius=0.33631052457814614 starId=-196901933 frame=false at=557220,0,-1629987 - body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=MOON orbit=21 radius=0.4688220383655022 starId=-196901933 frame=false at=-1860655,0,750736 - body 2553003_-535245_-1050940 2552999_-535245_-1050942 kind=MOON orbit=21 radius=0.6909717843738756 starId=-196901933 frame=false at=1857976,0,-1173240 - body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=GAS_GIANT orbit=220 radius=8.311637957512806 starId=-196901933 frame=true at=0,0,0 - body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.21016561089341101 starId=-196901933 frame=false at=-1863525,0,-132088 - body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.30216191398938774 starId=-196901933 frame=false at=-166931,0,-2046202 - body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.565144757070652 starId=-196901933 frame=false at=814570,0,1081569 - body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.5890525604324046 starId=-196901933 frame=false at=1690816,0,488611 - body 2553003_-535245_-1050940 2553001_-535245_-1050981 kind=MOON orbit=220 radius=0.6746556547268353 starId=-196901933 frame=false at=1024779,0,768961 - body 2553003_-535245_-1050940 2553002_-535245_-1050920 kind=PLANET orbit=105 radius=2.296973618840463 starId=-196901933 frame=true at=0,0,0 - body 2553003_-535245_-1050940 2553003_-535245_-1050940 kind=STAR orbit=0 radius=0.0 starId=-196901933 frame=true at=0,0,0 - body 2553003_-535245_-1050940 2553004_-535245_-1050942 kind=PLANET orbit=10 radius=0.6766914215527544 starId=-196901933 frame=true at=0,0,0 - body 2553003_-535245_-1050940 2553005_-535245_-1050941 kind=ASTEROID_BELT orbit=11 radius=0.0 starId=-196901933 frame=true at=0,0,0 - body 2553003_-535245_-1050940 2553009_-535245_-1050938 kind=PLANET orbit=34 radius=0.5304012228503437 starId=-196901933 frame=true at=0,0,0 - body 2553003_-535245_-1050940 2553035_-535241_-1051068 kind=ASTEROID_BELT orbit=704 radius=0.0 starId=-196901933 frame=true at=0,0,0 - body 2553003_-535245_-1050940 2553062_-535246_-1050997 kind=PLANET orbit=440 radius=0.3249400421494298 starId=-196901933 frame=true at=0,0,0 - body 3411017_1872378_2686670 3411017_1872378_2686670 kind=MOON orbit=0 radius=0.3524650663904568 starId=-1958754413 frame=false at=376684,0,-136342 - body 3411017_1872378_2686670 3411017_1872378_2686670 kind=MOON orbit=0 radius=1.4496425342156365 starId=-1958754413 frame=false at=271102,0,-165833 - body 3411017_1872378_2686670 3411017_1872378_2686670 kind=ROGUE_PLANET orbit=0 radius=1.603840597316356 starId=-1958754413 frame=true at=0,0,0 - body 3631783_-3208848_-3037694 3631783_-3208848_-3037694 kind=ROGUE_PLANET orbit=0 radius=1.152239748699427 starId=-43961145 frame=true at=0,0,0 - body 395746_2934615_-3374968 395746_2934615_-3374968 kind=MOON orbit=0 radius=1.3734148264964914 starId=-1652144877 frame=false at=20980,0,6623 - body 395746_2934615_-3374968 395746_2934615_-3374968 kind=ROGUE_PLANET orbit=0 radius=0.22103981078535706 starId=-1652144877 frame=true at=0,0,0 - body 4348163_4426685_2048679 4348163_4426685_2048679 kind=MOON orbit=0 radius=0.22387906715845088 starId=-1258055601 frame=false at=52859,0,101681 - body 4348163_4426685_2048679 4348163_4426685_2048679 kind=MOON orbit=0 radius=1.9102745823110474 starId=-1258055601 frame=false at=-17435,0,-285067 - body 4348163_4426685_2048679 4348163_4426685_2048679 kind=ROGUE_PLANET orbit=0 radius=0.9424057339265934 starId=-1258055601 frame=true at=0,0,0 - body 4836351_4419696_6894907 4836311_4419696_6894896 kind=PLANET orbit=222 radius=1.544147080458167 starId=-1565633077 frame=true at=0,0,0 - body 4836351_4419696_6894907 4836340_4419696_6894906 kind=GAS_GIANT orbit=57 radius=10.24002265252395 starId=-1565633077 frame=true at=0,0,0 - body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.20147518559961805 starId=-1565633077 frame=false at=581025,0,467576 - body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.45081664972260455 starId=-1565633077 frame=false at=878658,0,-554884 - body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.46322695463417846 starId=-1565633077 frame=false at=-2464488,0,-1133356 - body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.6489701106320351 starId=-1565633077 frame=false at=-1191490,0,-1549204 - body 4836351_4419696_6894907 4836340_4419696_6894906 kind=MOON orbit=57 radius=0.7380916191408169 starId=-1565633077 frame=false at=916270,0,-264200 - body 4836351_4419696_6894907 4836350_4419696_6894904 kind=ASTEROID_BELT orbit=15 radius=0.0 starId=-1565633077 frame=true at=0,0,0 - body 4836351_4419696_6894907 4836351_4419696_6894902 kind=GAS_GIANT orbit=27 radius=3.402256404961512 starId=-1565633077 frame=true at=0,0,0 - body 4836351_4419696_6894907 4836351_4419696_6894907 kind=STAR orbit=0 radius=0.0 starId=-1565633077 frame=true at=0,0,0 - body 4836351_4419696_6894907 4836351_4419696_6894909 kind=PLANET orbit=9 radius=0.20845924847231542 starId=-1565633077 frame=true at=0,0,0 - body 4836351_4419696_6894907 4836371_4419695_6894919 kind=PLANET orbit=126 radius=0.3528799763588127 starId=-1565633077 frame=true at=0,0,0 - body 4836351_4419696_6894907 4836414_4419699_6894927 kind=ASTEROID_BELT orbit=355 radius=0.0 starId=-1565633077 frame=true at=0,0,0 - body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=GAS_GIANT orbit=257 radius=10.283996064694179 starId=-893135689 frame=true at=0,0,0 - body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.20127083174628194 starId=-893135689 frame=false at=-432171,0,2295879 - body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.3764121020262485 starId=-893135689 frame=false at=-582054,0,-671434 - body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.40473385249339855 starId=-893135689 frame=false at=461445,0,-664736 - body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.7009865591178472 starId=-893135689 frame=false at=-949926,0,-1103439 - body 5393152_-2277199_2439904 5393106_-2277200_2439916 kind=MOON orbit=257 radius=0.7359933058039947 starId=-893135689 frame=false at=1019184,0,925955 - body 5393152_-2277199_2439904 5393136_-2277200_2439909 kind=GAS_GIANT orbit=89 radius=6.858044787609019 starId=-893135689 frame=true at=0,0,0 - body 5393152_-2277199_2439904 5393136_-2277200_2439909 kind=MOON orbit=89 radius=0.3398250696919669 starId=-893135689 frame=false at=-419343,0,580351 - body 5393152_-2277199_2439904 5393136_-2277200_2439909 kind=MOON orbit=89 radius=0.3467426497803343 starId=-893135689 frame=false at=-335138,0,-285718 - body 5393152_-2277199_2439904 5393136_-2277200_2439909 kind=MOON orbit=89 radius=0.7406934049445608 starId=-893135689 frame=false at=-1806393,0,-681860 - body 5393152_-2277199_2439904 5393151_-2277199_2439895 kind=ASTEROID_BELT orbit=49 radius=0.0 starId=-893135689 frame=true at=0,0,0 - body 5393152_-2277199_2439904 5393152_-2277199_2439904 kind=STAR orbit=0 radius=0.0 starId=-893135689 frame=true at=0,0,0 - body 5393152_-2277199_2439904 5393153_-2277199_2439905 kind=PLANET orbit=9 radius=1.2469482547976387 starId=-893135689 frame=true at=0,0,0 - body 5393152_-2277199_2439904 5393160_-2277199_2439900 kind=MOON orbit=46 radius=0.20402162240276098 starId=-893135689 frame=false at=-112747,0,-239156 - body 5393152_-2277199_2439904 5393160_-2277199_2439900 kind=MOON orbit=46 radius=0.47671909908637755 starId=-893135689 frame=false at=210052,0,146336 - body 5393152_-2277199_2439904 5393160_-2277199_2439900 kind=PLANET orbit=46 radius=0.9514122875049971 starId=-893135689 frame=true at=0,0,0 - body 5393152_-2277199_2439904 5393200_-2277200_2439964 kind=ASTEROID_BELT orbit=411 radius=0.0 starId=-893135689 frame=true at=0,0,0 - body 5854133_275080_2604160 5854133_275080_2604160 kind=MOON orbit=0 radius=2.238060855684214 starId=-1515893089 frame=false at=106472,0,-351216 - body 5854133_275080_2604160 5854133_275080_2604160 kind=ROGUE_PLANET orbit=0 radius=1.400785633512131 starId=-1515893089 frame=true at=0,0,0 - body 5891934_-757386_5055698 5891934_-757386_5055698 kind=MOON orbit=0 radius=1.308200595402922 starId=-1906018109 frame=false at=-99690,0,-221171 - body 5891934_-757386_5055698 5891934_-757386_5055698 kind=ROGUE_PLANET orbit=0 radius=1.893272712804346 starId=-1906018109 frame=true at=0,0,0 - body 6386779_6218703_-2290675 6386779_6218703_-2290675 kind=MOON orbit=0 radius=0.2600017031861839 starId=-788062221 frame=false at=-202560,0,34037 - body 6386779_6218703_-2290675 6386779_6218703_-2290675 kind=MOON orbit=0 radius=2.430948757116413 starId=-788062221 frame=false at=52230,0,-149753 - body 6386779_6218703_-2290675 6386779_6218703_-2290675 kind=ROGUE_PLANET orbit=0 radius=1.1355178221934288 starId=-788062221 frame=true at=0,0,0 - body 6918713_2294940_-820156 6918708_2294940_-820150 kind=MOON orbit=43 radius=0.324471229410524 starId=-1120847449 frame=false at=221671,0,413988 - body 6918713_2294940_-820156 6918708_2294940_-820150 kind=MOON orbit=43 radius=0.41469569651648625 starId=-1120847449 frame=false at=-232412,0,-19159 - body 6918713_2294940_-820156 6918708_2294940_-820150 kind=PLANET orbit=43 radius=1.6772381770058735 starId=-1120847449 frame=true at=0,0,0 - body 6918713_2294940_-820156 6918712_2294940_-820152 kind=MOON orbit=22 radius=0.22253218956390367 starId=-1120847449 frame=false at=21983,0,13884 - body 6918713_2294940_-820156 6918712_2294940_-820152 kind=MOON orbit=22 radius=0.2298457183025389 starId=-1120847449 frame=false at=-24956,0,61748 - body 6918713_2294940_-820156 6918712_2294940_-820152 kind=PLANET orbit=22 radius=0.2625801459682835 starId=-1120847449 frame=true at=0,0,0 - body 6918713_2294940_-820156 6918713_2294940_-820156 kind=STAR orbit=0 radius=0.0 starId=-1120847449 frame=true at=0,0,0 - body 6918713_2294940_-820156 6918715_2294941_-820138 kind=MOON orbit=96 radius=0.5580329812757592 starId=-1120847449 frame=false at=-195270,0,55484 - body 6918713_2294940_-820156 6918715_2294941_-820138 kind=PLANET orbit=96 radius=2.26108180661609 starId=-1120847449 frame=true at=0,0,0 - body 6918713_2294940_-820156 6918743_2294939_-820129 kind=PLANET orbit=217 radius=1.8296530727395168 starId=-1120847449 frame=true at=0,0,0 - body 6918713_2294940_-820156 6918773_2294940_-820077 kind=PLANET orbit=529 radius=1.5512577339702143 starId=-1120847449 frame=true at=0,0,0 - body 6918713_2294940_-820156 6918912_2294932_-820033 kind=MOON orbit=1251 radius=0.42418680649418383 starId=-1120847449 frame=false at=180222,0,418439 - body 6918713_2294940_-820156 6918912_2294932_-820033 kind=PLANET orbit=1251 radius=1.5135313072630292 starId=-1120847449 frame=true at=0,0,0 - body 6918713_2294940_-820156 6919013_2294959_-820378 kind=ASTEROID_BELT orbit=2001 radius=0.0 starId=-1120847449 frame=true at=0,0,0 - body 6947973_1979802_5101896 6947973_1979802_5101896 kind=MOON orbit=0 radius=0.9725300949324311 starId=-1165534741 frame=false at=-33882,0,-362621 - body 6947973_1979802_5101896 6947973_1979802_5101896 kind=ROGUE_PLANET orbit=0 radius=1.4038279741834425 starId=-1165534741 frame=true at=0,0,0 - body 878562_5596671_-502705 877083_5596637_-502177 kind=ASTEROID_BELT orbit=8398 radius=0.0 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878506_5596691_-502296 kind=GAS_GIANT orbit=2210 radius=8.44842360519568 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878506_5596691_-502296 kind=MOON orbit=2210 radius=0.20663708684964743 starId=-1528641933 frame=false at=-507604,0,-1664731 - body 878562_5596671_-502705 878506_5596691_-502296 kind=MOON orbit=2210 radius=0.35080038054064144 starId=-1528641933 frame=false at=1869447,0,-1303810 - body 878562_5596671_-502705 878516_5596673_-502729 kind=GAS_GIANT orbit=279 radius=7.655802025985872 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878534_5596672_-502627 kind=PLANET orbit=442 radius=0.8168679635028027 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878553_5596671_-502711 kind=PLANET orbit=56 radius=1.0779650352367258 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878554_5596670_-502677 kind=ASTEROID_BELT orbit=155 radius=0.0 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878556_5596671_-502707 kind=PLANET orbit=35 radius=0.7163408393411195 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878558_5596671_-502734 kind=MOON orbit=155 radius=0.21301645293632387 starId=-1528641933 frame=false at=-27422,0,-63535 - body 878562_5596671_-502705 878558_5596671_-502734 kind=MOON orbit=155 radius=0.6471535720830919 starId=-1528641933 frame=false at=18589,0,47921 - body 878562_5596671_-502705 878558_5596671_-502734 kind=PLANET orbit=155 radius=0.581475535515763 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878559_5596671_-502685 kind=PLANET orbit=109 radius=1.041678706292689 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878562_5596671_-502705 kind=STAR orbit=0 radius=0.0 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878620_5596668_-502556 kind=PLANET orbit=857 radius=0.2455048607815313 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878732_5596624_-501739 kind=GAS_GIANT orbit=5249 radius=6.895570710616032 starId=-1528641933 frame=true at=0,0,0 - body 878562_5596671_-502705 878732_5596624_-501739 kind=MOON orbit=5249 radius=0.5214014902948397 starId=-1528641933 frame=false at=-381512,0,-728102 - body 878562_5596671_-502705 878732_5596624_-501739 kind=MOON orbit=5249 radius=0.5402142647908047 starId=-1528641933 frame=false at=-1068581,0,-731192 - body 878562_5596671_-502705 878777_5596663_-502864 kind=PLANET orbit=1431 radius=1.9146860220043695 starId=-1528641933 frame=true at=0,0,0 - derived -1093770_2928517_-2936619 -1093710_2928517_-2936631 type=ice mass=1.3964456469001738 radius=1.0640082304214584 gravity=123 pressure=1600 tempK=186 oxygen=false locked=false rings=false rotation=89617 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093714_2928520_-2936554 type=ice mass=0.32402193776600785 radius=0.7478926338807454 gravity=58 pressure=481 tempK=117 oxygen=false locked=false rings=false rotation=46328 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093748_2928518_-2936619 type=gasgiant mass=266.8955298371472 radius=10.193231645391162 gravity=257 pressure=1600 tempK=327 oxygen=false locked=false rings=false rotation=13026 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093750_2928505_-2936328 type=barren mass=0.0020613091983445507 radius=0.20027460427854427 gravity=5 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=16236 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093763_2928518_-2936631 type=greenhouse mass=1.8371551952464107 radius=1.1267386015753886 gravity=145 pressure=1519 tempK=347 oxygen=false locked=false rings=false rotation=9612 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093768_2928515_-2936470 type=gasgiant mass=287.5541922555389 radius=10.529056608271485 gravity=259 pressure=1600 tempK=126 oxygen=false locked=false rings=true rotation=10936 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093769_2928517_-2936617 type=barren mass=0.0031935937678598536 radius=0.20075632583676514 gravity=8 pressure=0 tempK=504 oxygen=false locked=true rings=false rotation=16072 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093770_2928517_-2936619 type=lava mass=0.30001901663593344 radius=0.7063537592628808 gravity=60 pressure=0 tempK=1817 oxygen=false locked=true rings=false rotation=31735 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093771_2928517_-2936631 type=desert mass=0.01092556873493221 radius=0.2851886889287729 gravity=13 pressure=0 tempK=219 oxygen=false locked=false rings=false rotation=12926 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093778_2928517_-2936610 type=barren mass=0.002478191150655063 radius=0.20067793516870133 gravity=6 pressure=0 tempK=225 oxygen=false locked=false rings=false rotation=8229 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093791_2928516_-2936641 type=gasgiant mass=20.358678404345994 radius=3.3296796457692377 gravity=184 pressure=1600 tempK=280 oxygen=false locked=false rings=true rotation=4853 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093811_2928515_-2936618 type=ice mass=0.03961721912249209 radius=0.41868787916123795 gravity=23 pressure=4 tempK=101 oxygen=false locked=false rings=false rotation=75676 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093828_2928521_-2936524 type=gasgiant mass=111.26411759923079 radius=6.967877626193331 gravity=229 pressure=1600 tempK=146 oxygen=false locked=false rings=true rotation=6402 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1093967_2928517_-2936558 type=icegiant mass=57.494080748624015 radius=5.229190104169739 gravity=210 pressure=1600 tempK=107 oxygen=false locked=false rings=true rotation=13505 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1093770_2928517_-2936619 -1094040_2928511_-2937000 type=superearth mass=14.189075810777892 radius=2.110920648460378 gravity=318 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=56348 metallicity=0.4495167056776552 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1332546_1631083_3243750 -1332546_1631083_3243750 type=barren mass=0.0030360464590692155 radius=0.20027007926536833 gravity=8 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=43387 metallicity=1.2805395851689565 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1502067_-1460437_4865055 -1502067_-1460437_4865055 type=barren mass=0.5474176307060243 radius=0.8722168368675713 gravity=72 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=48570 metallicity=1.377567820292009 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2327343_6217060_-3227882 -2327248_6217066_-3227948 type=superearth mass=5.382876056475847 radius=1.6695391816792997 gravity=193 pressure=1600 tempK=92 oxygen=false locked=false rings=false rotation=32615 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2327343_6217060_-3227882 -2327331_6217060_-3227869 type=gasgiant mass=85.26914373948553 radius=6.206626820771696 gravity=221 pressure=1600 tempK=215 oxygen=false locked=false rings=true rotation=6630 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2327343_6217060_-3227882 -2327343_6217060_-3227882 type=lava mass=2.4503154630596082 radius=1.267855119066724 gravity=152 pressure=30 tempK=1089 oxygen=false locked=true rings=false rotation=71827 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2327343_6217060_-3227882 -2327344_6217060_-3227874 type=barren mass=0.17022683762316365 radius=0.617305205704887 gravity=45 pressure=19 tempK=169 oxygen=false locked=true rings=false rotation=37052 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2327343_6217060_-3227882 -2327344_6217060_-3227879 type=desert mass=0.19760497225790083 radius=0.6433805032761719 gravity=48 pressure=7 tempK=264 oxygen=false locked=true rings=false rotation=36780 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2327343_6217060_-3227882 -2327353_6217060_-3227880 type=superearth mass=5.647858716195294 radius=1.580146346818058 gravity=226 pressure=1600 tempK=316 oxygen=false locked=false rings=false rotation=69550 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2327343_6217060_-3227882 -2327394_6217059_-3227934 type=barren mass=0.006064086898440579 radius=0.263927013854739 gravity=9 pressure=0 tempK=54 oxygen=false locked=false rings=false rotation=29496 metallicity=0.7031786884263901 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3264137_6862129_5633995 -3264137_6862129_5633995 type=ice mass=1.9774137805355403 radius=1.1459793325492187 gravity=151 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=21373 metallicity=0.6222741151459 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3272771_-2281626_-427326 -3272606_-2281621_-427320 type=superearth mass=4.071980468622733 radius=1.38557474240371 gravity=212 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=22898 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3272771_-2281626_-427326 -3272685_-2281627_-427268 type=icegiant mass=50.06954063214108 radius=4.924089526202804 gravity=207 pressure=1600 tempK=87 oxygen=false locked=false rings=true rotation=5152 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3272771_-2281626_-427326 -3272728_-2281624_-427344 type=ice mass=0.07395833217578861 radius=0.48115646573370274 gravity=32 pressure=72 tempK=56 oxygen=false locked=false rings=false rotation=12791 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3272771_-2281626_-427326 -3272756_-2281626_-427334 type=superearth mass=8.066179180661075 radius=1.6998160251693322 gravity=279 pressure=1600 tempK=237 oxygen=false locked=false rings=false rotation=66257 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3272771_-2281626_-427326 -3272770_-2281626_-427322 type=ocean mass=1.1994237875618499 radius=1.1261802381726516 gravity=95 pressure=276 tempK=313 oxygen=false locked=true rings=true rotation=82311 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3272771_-2281626_-427326 -3272770_-2281626_-427327 type=desert mass=1.0209010517542583 radius=1.0766820549511433 gravity=88 pressure=40 tempK=375 oxygen=false locked=true rings=true rotation=80510 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3272771_-2281626_-427326 -3272771_-2281626_-427326 type=barren mass=0.0034226765713115154 radius=0.21269833424610543 gravity=8 pressure=0 tempK=1052 oxygen=false locked=true rings=false rotation=72845 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3272771_-2281626_-427326 -3272775_-2281626_-427332 type=barren mass=0.029519389376990515 radius=0.4006648591407689 gravity=18 pressure=1 tempK=173 oxygen=false locked=true rings=false rotation=39886 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3272771_-2281626_-427326 -3272786_-2281624_-427271 type=ice mass=0.009627940168547167 radius=0.2734110634313652 gravity=13 pressure=1 tempK=49 oxygen=false locked=false rings=false rotation=21230 metallicity=1.0488153211000197 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3382269_4609397_102289 -3382162_4609391_102429 type=ice mass=0.08994449968247054 radius=0.5110071166882669 gravity=34 pressure=9 tempK=71 oxygen=false locked=false rings=false rotation=38907 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3382269_4609397_102289 -3382258_4609397_102291 type=greenhouse mass=2.0405581126019174 radius=1.1945742911747057 gravity=143 pressure=291 tempK=364 oxygen=false locked=false rings=false rotation=29026 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3382269_4609397_102289 -3382269_4609397_102289 type=lava mass=11.511494176857271 radius=2.040002113160309 gravity=277 pressure=62 tempK=2691 oxygen=false locked=true rings=false rotation=16716 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3382269_4609397_102289 -3382271_4609397_102293 type=greenhouse mass=22.281808900232875 radius=2.4553590705256463 gravity=370 pressure=1600 tempK=897 oxygen=false locked=true rings=false rotation=12918 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3382269_4609397_102289 -3382275_4609398_102253 type=ice mass=0.018314709335077232 radius=0.32843835951638584 gravity=17 pressure=0 tempK=158 oxygen=false locked=false rings=false rotation=21956 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3382269_4609397_102289 -3382347_4609393_102367 type=superearth mass=6.367156770825919 radius=1.7294190750209564 gravity=213 pressure=1600 tempK=233 oxygen=false locked=false rings=false rotation=6198 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3382269_4609397_102289 -3382624_4609397_102449 type=ice mass=0.7354859432149627 radius=0.8834632249979295 gravity=94 pressure=1600 tempK=108 oxygen=false locked=false rings=false rotation=43341 metallicity=0.8491613945038294 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -446119_-2387600_1528743 -430224_-2387600_1538055 type=ice mass=1.518364842304439 radius=1.0738446824738717 gravity=132 pressure=1600 tempK=5 oxygen=false locked=false rings=false rotation=19508 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -446119_-2387600_1528743 -446071_-2387599_1528765 type=ice mass=9.144461672947815 radius=1.8371459761843838 gravity=271 pressure=1600 tempK=96 oxygen=false locked=false rings=false rotation=32130 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -446119_-2387600_1528743 -446073_-2387603_1528672 type=icegiant mass=82.86029378521921 radius=6.1297755980796795 gravity=221 pressure=1600 tempK=80 oxygen=false locked=false rings=true rotation=11189 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -446119_-2387600_1528743 -446114_-2387600_1528734 type=icegiant mass=124.78965102386721 radius=7.324245297594027 gravity=233 pressure=1600 tempK=236 oxygen=false locked=false rings=true rotation=12164 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -446119_-2387600_1528743 -446116_-2387600_1528747 type=barren mass=0.12386907945589916 radius=0.5507257290050611 gravity=41 pressure=21 tempK=163 oxygen=false locked=true rings=false rotation=19690 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -446119_-2387600_1528743 -446118_-2387600_1528741 type=superearth mass=4.258372902034484 radius=1.3953997874664419 gravity=219 pressure=1600 tempK=520 oxygen=false locked=true rings=false rotation=39283 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -446119_-2387600_1528743 -446119_-2387600_1528743 type=barren mass=0.20274132430322034 radius=0.6666674967803836 gravity=46 pressure=0 tempK=882 oxygen=false locked=true rings=false rotation=6752 metallicity=0.42585638207427984 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -612264_1154834_5810641 -612264_1154834_5810641 type=ice mass=0.056942950901811534 radius=0.4898994165169739 gravity=24 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=95666 metallicity=1.1827308301341946 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1301912_-3086588_1331515 type=ice mass=6.727556099331668 radius=1.6761155997081671 gravity=239 pressure=1600 tempK=62 oxygen=false locked=false rings=false rotation=13868 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302043_-3086592_1332186 type=icegiant mass=314.28067515728264 radius=10.943876624718687 gravity=262 pressure=1600 tempK=147 oxygen=false locked=false rings=true rotation=10957 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302078_-3086584_1332016 type=gasgiant mass=26.276399487834283 radius=3.7203469240327802 gravity=190 pressure=1600 tempK=183 oxygen=false locked=false rings=true rotation=10263 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302107_-3086586_1332065 type=gasgiant mass=312.17149285060555 radius=10.911882841329781 gravity=262 pressure=1600 tempK=344 oxygen=false locked=false rings=true rotation=14381 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302109_-3086586_1332079 type=barren mass=0.0023510991180216495 radius=0.20168145982592578 gravity=6 pressure=0 tempK=266 oxygen=false locked=false rings=false rotation=20922 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302113_-3086586_1332071 type=ice mass=0.023236624649335194 radius=0.3769303000187614 gravity=16 pressure=0 tempK=177 oxygen=false locked=false rings=false rotation=16394 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302114_-3086586_1332089 type=greenhouse mass=15.24348811060426 radius=2.0413133728416963 gravity=366 pressure=1600 tempK=712 oxygen=false locked=true rings=false rotation=8527 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302116_-3086586_1332086 type=lava mass=7.532876687932706 radius=1.6663275403725344 gravity=271 pressure=49 tempK=1951 oxygen=false locked=true rings=false rotation=86063 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302118_-3086586_1332087 type=barren mass=0.013205677936849089 radius=0.3079462488366804 gravity=14 pressure=0 tempK=538 oxygen=false locked=true rings=true rotation=10129 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302121_-3086586_1332088 type=desert mass=0.1461314196694171 radius=0.6327276283379721 gravity=37 pressure=1 tempK=340 oxygen=false locked=true rings=false rotation=25342 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302129_-3086585_1332086 type=barren mass=0.0801573207690969 radius=0.5005886655353486 gravity=32 pressure=2 tempK=237 oxygen=false locked=false rings=false rotation=23399 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302140_-3086586_1332065 type=superearth mass=21.7845905298226 radius=2.3337921361694023 gravity=400 pressure=1600 tempK=317 oxygen=false locked=false rings=false rotation=6195 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302170_-3086588_1332078 type=ice mass=0.02443394862672128 radius=0.36453462415151583 gravity=18 pressure=2 tempK=92 oxygen=false locked=false rings=false rotation=41066 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302184_-3086594_1331905 type=ice mass=0.2054015059743704 radius=0.6157141695354549 gravity=54 pressure=799 tempK=93 oxygen=false locked=false rings=false rotation=40212 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1302116_-3086586_1332086 1302488_-3086594_1332155 type=gasgiant mass=255.5485641015431 radius=10.002498125514519 gravity=255 pressure=1600 tempK=84 oxygen=false locked=false rings=false rotation=6277 metallicity=0.43493676286617977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1609421_-1918473_4755340 1609421_-1918473_4755340 type=barren mass=0.46770924666133595 radius=0.7827230613996494 gravity=76 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=56250 metallicity=1.5936870208421627 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1899532_3100584_4509498 1899470_3100581_4509619 type=icegiant mass=165.69415081467872 radius=8.285086710521213 gravity=241 pressure=1600 tempK=74 oxygen=false locked=false rings=true rotation=8195 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1899532_3100584_4509498 1899514_3100584_4509415 type=ice mass=0.003056167890674599 radius=0.20516492708996092 gravity=7 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=28382 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1899532_3100584_4509498 1899521_3100583_4509481 type=barren mass=0.018305767481150027 radius=0.32640763764967407 gravity=17 pressure=0 tempK=98 oxygen=false locked=false rings=false rotation=7838 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1899532_3100584_4509498 1899531_3100584_4509493 type=ice mass=0.41134681597415607 radius=0.8240496319587478 gravity=61 pressure=57 tempK=168 oxygen=false locked=true rings=false rotation=6059 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1899532_3100584_4509498 1899532_3100584_4509497 type=greenhouse mass=2.037437940104025 radius=1.278950213656414 gravity=125 pressure=234 tempK=394 oxygen=false locked=true rings=false rotation=41187 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1899532_3100584_4509498 1899532_3100584_4509498 type=lava mass=1.897278835122145 radius=1.2562049753102442 gravity=120 pressure=16 tempK=1032 oxygen=false locked=true rings=false rotation=40884 metallicity=0.7166566927880405 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2031833_5218371_1147521 2031833_5218371_1147521 type=ice mass=2.5510869474691416 radius=1.2501648106704106 gravity=163 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=11654 metallicity=1.4459712310561943 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2106667_5115784_6590371 2106667_5115784_6590371 type=ice mass=4.249072418266108 radius=1.4995482876777844 gravity=189 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=9721 metallicity=0.4194434083178827 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2553003_-535245_-1050940 2552999_-535245_-1050942 type=gasgiant mass=227.5677715055987 radius=9.510681930029026 gravity=252 pressure=1600 tempK=456 oxygen=false locked=false rings=true rotation=6946 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2553003_-535245_-1050940 2553001_-535245_-1050981 type=icegiant mass=166.9179971362554 radius=8.311637957512806 gravity=242 pressure=1600 tempK=141 oxygen=false locked=false rings=true rotation=9106 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2553003_-535245_-1050940 2553002_-535245_-1050920 type=ice mass=24.411665674028523 radius=2.296973618840463 gravity=400 pressure=1600 tempK=193 oxygen=false locked=false rings=false rotation=36253 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2553003_-535245_-1050940 2553003_-535245_-1050940 type=lava mass=1.900550028726414 radius=1.2839913266496088 gravity=115 pressure=13 tempK=1078 oxygen=false locked=true rings=false rotation=12040 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2553003_-535245_-1050940 2553004_-535245_-1050942 type=desert mass=0.2567533757830658 radius=0.6766914215527544 gravity=56 pressure=7 tempK=320 oxygen=false locked=true rings=false rotation=15749 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2553003_-535245_-1050940 2553005_-535245_-1050941 type=desert mass=1.844656909969501 radius=1.2246704181014183 gravity=123 pressure=131 tempK=367 oxygen=false locked=true rings=false rotation=95242 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2553003_-535245_-1050940 2553009_-535245_-1050938 type=barren mass=0.08530333989449732 radius=0.5304012228503437 gravity=30 pressure=3 tempK=183 oxygen=false locked=true rings=false rotation=31022 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2553003_-535245_-1050940 2553035_-535241_-1051068 type=ice mass=0.0027330751712316757 radius=0.20217275170982937 gravity=7 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=87702 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2553003_-535245_-1050940 2553062_-535246_-1050997 type=barren mass=0.017356852428609234 radius=0.3249400421494298 gravity=16 pressure=4 tempK=51 oxygen=false locked=false rings=false rotation=82116 metallicity=1.5136774822282644 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3411017_1872378_2686670 3411017_1872378_2686670 type=superearth mass=4.952184770820822 radius=1.603840597316356 gravity=193 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=11340 metallicity=1.1462273141153667 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3631783_-3208848_-3037694 3631783_-3208848_-3037694 type=ice mass=1.9425587511821718 radius=1.152239748699427 gravity=146 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=6687 metallicity=1.4047027502857872 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 395746_2934615_-3374968 395746_2934615_-3374968 type=barren mass=0.004214204412657573 radius=0.22103981078535706 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=75854 metallicity=0.7565338861017625 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4348163_4426685_2048679 4348163_4426685_2048679 type=ice mass=0.6475717025036999 radius=0.9424057339265934 gravity=73 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=7445 metallicity=1.2663943364164263 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4836351_4419696_6894907 4836311_4419696_6894896 type=ice mass=4.004980936282541 radius=1.544147080458167 gravity=168 pressure=1600 tempK=104 oxygen=false locked=false rings=false rotation=15044 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4836351_4419696_6894907 4836340_4419696_6894906 type=gasgiant mass=269.7218029621221 radius=10.24002265252395 gravity=257 pressure=1600 tempK=218 oxygen=false locked=false rings=true rotation=5316 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4836351_4419696_6894907 4836350_4419696_6894904 type=barren mass=0.013500375504041259 radius=0.3242170760793095 gravity=13 pressure=0 tempK=217 oxygen=false locked=true rings=false rotation=20318 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4836351_4419696_6894907 4836351_4419696_6894902 type=gasgiant mass=21.393810118666952 radius=3.402256404961512 gravity=185 pressure=1600 tempK=317 oxygen=false locked=false rings=true rotation=11193 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4836351_4419696_6894907 4836351_4419696_6894907 type=barren mass=0.002482848571723704 radius=0.20323854566294994 gravity=6 pressure=0 tempK=843 oxygen=false locked=true rings=false rotation=46184 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4836351_4419696_6894907 4836351_4419696_6894909 type=barren mass=0.0027190919055376955 radius=0.20845924847231542 gravity=6 pressure=0 tempK=281 oxygen=false locked=true rings=false rotation=46871 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4836351_4419696_6894907 4836371_4419695_6894919 type=barren mass=0.016383987712612607 radius=0.3528799763588127 gravity=13 pressure=3 tempK=75 oxygen=false locked=false rings=false rotation=25396 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4836351_4419696_6894907 4836414_4419699_6894927 type=superearth mass=21.736906495855145 radius=2.188905473138211 gravity=400 pressure=1600 tempK=95 oxygen=false locked=false rings=false rotation=69924 metallicity=0.7318991324516715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5393152_-2277199_2439904 5393106_-2277200_2439916 type=icegiant mass=272.39323545820196 radius=10.283996064694179 gravity=258 pressure=1600 tempK=122 oxygen=false locked=false rings=true rotation=8051 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5393152_-2277199_2439904 5393136_-2277200_2439909 type=icegiant mass=107.27157898441754 radius=6.858044787609019 gravity=228 pressure=1600 tempK=208 oxygen=false locked=false rings=true rotation=6030 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5393152_-2277199_2439904 5393151_-2277199_2439895 type=superearth mass=19.492106006194042 radius=2.186045391780912 gravity=400 pressure=1600 tempK=305 oxygen=false locked=false rings=false rotation=13463 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5393152_-2277199_2439904 5393152_-2277199_2439904 type=lava mass=1.0396455318374358 radius=1.0476581912821665 gravity=95 pressure=2 tempK=1011 oxygen=false locked=true rings=false rotation=79360 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5393152_-2277199_2439904 5393153_-2277199_2439905 type=greenhouse mass=2.0705829669089 radius=1.2469482547976387 gravity=133 pressure=437 tempK=398 oxygen=false locked=true rings=false rotation=49207 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5393152_-2277199_2439904 5393160_-2277199_2439900 type=ice mass=0.8085225439795893 radius=0.9514122875049971 gravity=89 pressure=155 tempK=152 oxygen=false locked=true rings=false rotation=6190 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5393152_-2277199_2439904 5393200_-2277200_2439964 type=ice mass=6.911285732211827 radius=1.6276446283068091 gravity=261 pressure=1600 tempK=91 oxygen=false locked=false rings=false rotation=7608 metallicity=0.4019032606148522 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5854133_275080_2604160 5854133_275080_2604160 type=ice mass=3.580183683247193 radius=1.400785633512131 gravity=182 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=48181 metallicity=0.6292643178557435 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5891934_-757386_5055698 5891934_-757386_5055698 type=superearth mass=12.048504252431522 radius=1.893272712804346 gravity=336 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=14494 metallicity=1.3821825544882356 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6386779_6218703_-2290675 6386779_6218703_-2290675 type=ice mass=1.7551791814995448 radius=1.1355178221934288 gravity=136 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=7432 metallicity=1.0247030625294358 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6918713_2294940_-820156 6918708_2294940_-820150 type=lava mass=5.403681358397993 radius=1.6772381770058735 gravity=192 pressure=1600 tempK=712 oxygen=false locked=true rings=false rotation=8377 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6918713_2294940_-820156 6918712_2294940_-820152 type=desert mass=0.00875180556812671 radius=0.2625801459682835 gravity=13 pressure=0 tempK=415 oxygen=false locked=true rings=false rotation=91877 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6918713_2294940_-820156 6918713_2294940_-820156 type=lava mass=7.749429510714092 radius=1.8364052409673304 gravity=230 pressure=16 tempK=2075 oxygen=false locked=true rings=false rotation=34493 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6918713_2294940_-820156 6918715_2294941_-820138 type=greenhouse mass=17.249323402630562 radius=2.26108180661609 gravity=337 pressure=1600 tempK=346 oxygen=false locked=false rings=true rotation=23547 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6918713_2294940_-820156 6918743_2294939_-820129 type=superearth mass=10.27636828957459 radius=1.8296530727395168 gravity=307 pressure=1600 tempK=297 oxygen=false locked=false rings=false rotation=17054 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6918713_2294940_-820156 6918773_2294940_-820077 type=ice mass=4.391763206778984 radius=1.5512577339702143 gravity=183 pressure=1600 tempK=165 oxygen=false locked=false rings=false rotation=38168 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6918713_2294940_-820156 6918912_2294932_-820033 type=superearth mass=4.98426413590334 radius=1.5135313072630292 gravity=218 pressure=1600 tempK=123 oxygen=false locked=false rings=false rotation=15355 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6918713_2294940_-820156 6919013_2294959_-820378 type=icegiant mass=129.09205638035172 radius=7.432985633827182 gravity=234 pressure=1600 tempK=90 oxygen=false locked=false rings=true rotation=6371 metallicity=1.3668517738430017 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6947973_1979802_5101896 6947973_1979802_5101896 type=superearth mass=4.2554336056184825 radius=1.4038279741834425 gravity=216 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=11724 metallicity=0.41150504664655163 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 877083_5596637_-502177 type=barren mass=0.0043929527254645065 radius=0.21733584703571496 gravity=9 pressure=1 tempK=33 oxygen=false locked=false rings=false rotation=8140 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878506_5596691_-502296 type=gasgiant mass=173.30377033278236 radius=8.44842360519568 gravity=243 pressure=1600 tempK=128 oxygen=false locked=false rings=true rotation=5787 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878516_5596673_-502729 type=gasgiant mass=138.16643154342793 radius=7.655802025985872 gravity=236 pressure=1600 tempK=361 oxygen=false locked=false rings=true rotation=5793 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878534_5596672_-502627 type=ice mass=0.4329737640819448 radius=0.8168679635028027 gravity=65 pressure=71 tempK=124 oxygen=false locked=false rings=false rotation=12516 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878553_5596671_-502711 type=desert mass=1.4045176765774283 radius=1.0779650352367258 gravity=121 pressure=131 tempK=469 oxygen=false locked=false rings=false rotation=42405 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878554_5596670_-502677 type=barren mass=0.01659112176307928 radius=0.32824293768225865 gravity=15 pressure=0 tempK=248 oxygen=false locked=false rings=false rotation=8690 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878556_5596671_-502707 type=barren mass=0.34852268140098014 radius=0.7163408393411195 gravity=68 pressure=7 tempK=522 oxygen=false locked=true rings=false rotation=37661 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878558_5596671_-502734 type=desert mass=0.1311490601758935 radius=0.581475535515763 gravity=39 pressure=6 tempK=234 oxygen=false locked=false rings=false rotation=32520 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878559_5596671_-502685 type=desert mass=0.9618089904813903 radius=1.041678706292689 gravity=89 pressure=158 tempK=352 oxygen=false locked=false rings=false rotation=70372 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878562_5596671_-502705 type=lava mass=0.0022248877370953236 radius=0.20544840880774495 gravity=5 pressure=0 tempK=3109 oxygen=false locked=true rings=false rotation=10975 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878620_5596668_-502556 type=barren mass=0.006223516414818935 radius=0.2455048607815313 gravity=10 pressure=0 tempK=105 oxygen=false locked=false rings=false rotation=27645 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878732_5596624_-501739 type=icegiant mass=108.62641371674347 radius=6.895570710616032 gravity=228 pressure=1600 tempK=83 oxygen=false locked=false rings=true rotation=6989 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 878562_5596671_-502705 878777_5596663_-502864 type=ice mass=9.246446677537232 radius=1.9146860220043695 gravity=252 pressure=1600 tempK=151 oxygen=false locked=false rings=false rotation=8820 metallicity=1.1072686805278245 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1093770_2928517_-2936619 id=-953287813 kind=STAR name=PGS--3525313.0.-3525313 starTemp=70 starSize=0.908119261264801 - system -1332546_1631083_3243750 id=-1436132233 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless - system -1502067_-1460437_4865055 id=-1542560749 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless - system -2327343_6217060_-3227882 id=-638227073 kind=STAR name=PGS--3525313.3525313.-3525313 starTemp=40 starSize=0.998796284198761 - system -3264137_6862129_5633995 id=-1878334477 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless - system -3272771_-2281626_-427326 id=-1787505529 kind=STAR name=PGS--3525313.-3525313.-3525313 starTemp=40 starSize=0.9442926049232483 - system -3382269_4609397_102289 id=-1804580365 kind=STAR name=PGS--3525313.3525313.0 starTemp=100 starSize=0.9761362671852112 - system -446119_-2387600_1528743 id=-1238572757 kind=STAR name=PGS--3525313.-3525313.0 starTemp=40 starSize=0.6638630628585815 - system -612264_1154834_5810641 id=-912475673 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless - system 1302116_-3086586_1332086 id=-993759433 kind=STAR name=PGS-0.-3525313.0 starTemp=70 starSize=1.0476429462432861 - system 1609421_-1918473_4755340 id=-57202221 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless - system 1899532_3100584_4509498 id=-810501533 kind=STAR name=PGS-0.0.3525313 starTemp=40 starSize=0.8984453082084656 - system 2031833_5218371_1147521 id=-770357401 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless - system 2106667_5115784_6590371 id=-714236625 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless - system 2553003_-535245_-1050940 id=-196901933 kind=STAR name=PGS-0.-3525313.-3525313 starTemp=40 starSize=0.9787595868110657 - system 3411017_1872378_2686670 id=-1958754413 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 3631783_-3208848_-3037694 id=-43961145 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless - system 395746_2934615_-3374968 id=-1652144877 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless - system 4348163_4426685_2048679 id=-1258055601 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless - system 4836351_4419696_6894907 id=-1565633077 kind=STAR name=PGS-3525313.3525313.3525313 starTemp=40 starSize=0.605627179145813 - system 5393152_-2277199_2439904 id=-893135689 kind=STAR name=PGS-3525313.-3525313.0 starTemp=40 starSize=0.862093448638916 - system 5854133_275080_2604160 id=-1515893089 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless - system 5891934_-757386_5055698 id=-1906018109 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless - system 6386779_6218703_-2290675 id=-788062221 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless - system 6918713_2294940_-820156 id=-1120847449 kind=STAR name=PGS-3525313.0.-3525313 starTemp=70 starSize=1.1846755743026733 - system 6947973_1979802_5101896 id=-1165534741 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless - system 878562_5596671_-502705 id=-1528641933 kind=STAR name=PGS-0.3525313.-3525313 starTemp=100 starSize=1.3026405572891235 -seed 1337 systems=27 - body -1923947_-1394043_-2837349 -1923947_-1394043_-2837349 kind=MOON orbit=0 radius=0.3504343535747473 starId=-1836287757 frame=false at=45262,0,21712 - body -1923947_-1394043_-2837349 -1923947_-1394043_-2837349 kind=MOON orbit=0 radius=1.0576569744419213 starId=-1836287757 frame=false at=112401,0,-19027 - body -1923947_-1394043_-2837349 -1923947_-1394043_-2837349 kind=ROGUE_PLANET orbit=0 radius=0.5105195562020988 starId=-1836287757 frame=true at=0,0,0 - body -2547136_6553750_-2327906 -2547136_6553750_-2327906 kind=MOON orbit=0 radius=0.900273147073968 starId=-384211249 frame=false at=112983,0,-147500 - body -2547136_6553750_-2327906 -2547136_6553750_-2327906 kind=MOON orbit=0 radius=1.85660762023691 starId=-384211249 frame=false at=-6167,0,-88786 - body -2547136_6553750_-2327906 -2547136_6553750_-2327906 kind=ROGUE_PLANET orbit=0 radius=0.9094368622429794 starId=-384211249 frame=true at=0,0,0 - body -3335279_-2608304_4283952 -3335279_-2608304_4283952 kind=ROGUE_PLANET orbit=0 radius=1.052788722587457 starId=-783595273 frame=true at=0,0,0 - body -3347275_169917_-2040789 -3347275_169917_-2040789 kind=MOON orbit=0 radius=0.41891985661068964 starId=-1080499465 frame=false at=-338885,0,126140 - body -3347275_169917_-2040789 -3347275_169917_-2040789 kind=ROGUE_PLANET orbit=0 radius=1.9202731970800502 starId=-1080499465 frame=true at=0,0,0 - body -3350400_5100916_4798244 -3350400_5100916_4798244 kind=ROGUE_PLANET orbit=0 radius=0.605114195212058 starId=-513667701 frame=true at=0,0,0 - body -428001_4188003_1035570 -428001_4188003_1035570 kind=ROGUE_PLANET orbit=0 radius=1.1126961933492066 starId=-1929605705 frame=true at=0,0,0 - body -429399_-975487_1152563 -428852_-975510_1152659 kind=ASTEROID_BELT orbit=2972 radius=0.0 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429309_-975487_1152547 kind=GAS_GIANT orbit=488 radius=9.868809637033028 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429309_-975487_1152547 kind=MOON orbit=488 radius=0.500451212795628 starId=-751569025 frame=false at=516972,0,2941517 - body -429399_-975487_1152563 -429373_-975485_1152519 kind=ASTEROID_BELT orbit=271 radius=0.0 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429376_-975486_1152564 kind=MOON orbit=125 radius=0.20907843201502585 starId=-751569025 frame=false at=14863,0,-36489 - body -429399_-975487_1152563 -429376_-975486_1152564 kind=PLANET orbit=125 radius=0.3074005137389715 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429390_-975487_1152605 kind=MOON orbit=232 radius=0.24326546571335786 starId=-751569025 frame=false at=-28759,0,-56502 - body -429399_-975487_1152563 -429390_-975487_1152605 kind=MOON orbit=232 radius=0.7421202957180757 starId=-751569025 frame=false at=41961,0,-29338 - body -429399_-975487_1152563 -429390_-975487_1152605 kind=PLANET orbit=232 radius=0.325630568968772 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429399_-975487_1152563 kind=STAR orbit=0 radius=0.0 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429401_-975487_1152561 kind=PLANET orbit=14 radius=1.7488084514421833 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429404_-975487_1152562 kind=PLANET orbit=27 radius=0.7162074824220463 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429404_-975487_1152573 kind=PLANET orbit=58 radius=0.7218413062090878 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429544_-975483_1152487 kind=MOON orbit=877 radius=0.49083928585913217 starId=-751569025 frame=false at=-82351,0,293672 - body -429399_-975487_1152563 -429544_-975483_1152487 kind=PLANET orbit=877 radius=1.8057296310075321 starId=-751569025 frame=true at=0,0,0 - body -429399_-975487_1152563 -429718_-975476_1152699 kind=MOON orbit=1858 radius=0.24438618987929492 starId=-751569025 frame=false at=-11670,0,99317 - body -429399_-975487_1152563 -429718_-975476_1152699 kind=PLANET orbit=1858 radius=0.3279921891899943 starId=-751569025 frame=true at=0,0,0 - body -565986_1972460_5527035 -565986_1972460_5527035 kind=MOON orbit=0 radius=0.20311774682099143 starId=-1194177805 frame=false at=37385,0,11790 - body -565986_1972460_5527035 -565986_1972460_5527035 kind=ROGUE_PLANET orbit=0 radius=0.20510853917918126 starId=-1194177805 frame=true at=0,0,0 - body -810232_3115510_2746902 -810232_3115510_2746902 kind=ROGUE_PLANET orbit=0 radius=1.7208896712788562 starId=-1645051273 frame=true at=0,0,0 - body 1713158_-2537384_2172172 1713158_-2537384_2172172 kind=ROGUE_PLANET orbit=0 radius=0.7718606386627891 starId=-1753285501 frame=true at=0,0,0 - body 1737362_-2563157_5310115 1737362_-2563157_5310115 kind=ROGUE_PLANET orbit=0 radius=0.24063193054985418 starId=-1864487113 frame=true at=0,0,0 - body 237817_5592419_4203270 237817_5592419_4203270 kind=ROGUE_PLANET orbit=0 radius=1.4298526622976593 starId=-1978443773 frame=true at=0,0,0 - body 2414414_3713625_-2289058 2414414_3713625_-2289058 kind=ROGUE_PLANET orbit=0 radius=0.8921379780825387 starId=-352841721 frame=true at=0,0,0 - body 2528228_-231370_-794020 2528228_-231370_-794020 kind=ROGUE_PLANET orbit=0 radius=1.6503145995487494 starId=-1916009805 frame=true at=0,0,0 - body 2663742_1091380_-601497 2663369_1091393_-601424 kind=ASTEROID_BELT orbit=2032 radius=0.0 starId=-824346553 frame=true at=0,0,0 - body 2663742_1091380_-601497 2663727_1091381_-601507 kind=ASTEROID_BELT orbit=97 radius=0.0 starId=-824346553 frame=true at=0,0,0 - body 2663742_1091380_-601497 2663742_1091380_-601497 kind=STAR orbit=0 radius=0.0 starId=-824346553 frame=true at=0,0,0 - body 2663742_1091380_-601497 2663746_1091380_-601495 kind=PLANET orbit=22 radius=0.654463219883428 starId=-824346553 frame=true at=0,0,0 - body 2663742_1091380_-601497 2663756_1091380_-601527 kind=GAS_GIANT orbit=176 radius=7.03735373269887 starId=-824346553 frame=true at=0,0,0 - body 2663742_1091380_-601497 2663946_1091379_-601618 kind=MOON orbit=1270 radius=0.6340384622197031 starId=-824346553 frame=false at=6458,0,-37650 - body 2663742_1091380_-601497 2663946_1091379_-601618 kind=PLANET orbit=1270 radius=0.3669832544972964 starId=-824346553 frame=true at=0,0,0 - body 2680390_5368406_2491985 2680390_5368406_2491985 kind=MOON orbit=0 radius=0.2657251222699931 starId=-1809354021 frame=false at=-26076,0,129807 - body 2680390_5368406_2491985 2680390_5368406_2491985 kind=MOON orbit=0 radius=0.3621595202802164 starId=-1809354021 frame=false at=-118064,0,-148865 - body 2680390_5368406_2491985 2680390_5368406_2491985 kind=ROGUE_PLANET orbit=0 radius=0.6291674918965895 starId=-1809354021 frame=true at=0,0,0 - body 2779562_651884_2693855 2779562_651884_2693855 kind=MOON orbit=0 radius=1.8740284163883787 starId=-342178325 frame=false at=114312,0,306369 - body 2779562_651884_2693855 2779562_651884_2693855 kind=ROGUE_PLANET orbit=0 radius=1.5977631514755333 starId=-342178325 frame=true at=0,0,0 - body 3951440_4496837_903895 3951440_4496837_903895 kind=ROGUE_PLANET orbit=0 radius=0.2824811542361627 starId=-610246349 frame=true at=0,0,0 - body 4245760_1904115_3414082 4245760_1904115_3414082 kind=MOON orbit=0 radius=0.7516842566655517 starId=-847248597 frame=false at=23114,0,246117 - body 4245760_1904115_3414082 4245760_1904115_3414082 kind=MOON orbit=0 radius=1.855151261146064 starId=-847248597 frame=false at=-98334,0,69474 - body 4245760_1904115_3414082 4245760_1904115_3414082 kind=ROGUE_PLANET orbit=0 radius=1.1983557194239915 starId=-847248597 frame=true at=0,0,0 - body 4758850_1796822_6525935 4758850_1796822_6525935 kind=ROGUE_PLANET orbit=0 radius=2.430158213292819 starId=-1218084841 frame=true at=0,0,0 - body 4871731_-1898639_-2858414 4871373_-1898655_-2858243 kind=PLANET orbit=2123 radius=0.20276781582524947 starId=-782061137 frame=true at=0,0,0 - body 4871731_-1898639_-2858414 4871730_-1898639_-2858397 kind=PLANET orbit=89 radius=1.1128743239382588 starId=-782061137 frame=true at=0,0,0 - body 4871731_-1898639_-2858414 4871731_-1898639_-2858414 kind=STAR orbit=0 radius=0.0 starId=-782061137 frame=true at=0,0,0 - body 4871731_-1898639_-2858414 4871733_-1898639_-2858412 kind=STAR orbit=16 radius=86.23844600737095 starId=-782061138 frame=true at=0,0,0 - body 4871731_-1898639_-2858414 4871781_-1898639_-2858386 kind=ASTEROID_BELT orbit=305 radius=0.0 starId=-782061137 frame=true at=0,0,0 - body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=GAS_GIANT orbit=549 radius=6.469918942207257 starId=-782061137 frame=true at=0,0,0 - body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=MOON orbit=549 radius=0.243831978323108 starId=-782061137 frame=false at=-128192,0,-659660 - body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=MOON orbit=549 radius=0.2638527372015568 starId=-782061137 frame=false at=-1532887,0,-626572 - body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=MOON orbit=549 radius=0.2771452170699142 starId=-782061137 frame=false at=-1264281,0,-176920 - body 4871731_-1898639_-2858414 4871804_-1898637_-2858342 kind=MOON orbit=549 radius=0.45642043423103856 starId=-782061137 frame=false at=1368418,0,65829 - body 4871731_-1898639_-2858414 4871865_-1898612_-2859034 kind=ASTEROID_BELT orbit=3396 radius=0.0 starId=-782061137 frame=true at=0,0,0 - body 5051436_4723973_4799957 5051436_4723973_4799957 kind=ROGUE_PLANET orbit=0 radius=0.3044740134514678 starId=-1829532925 frame=true at=0,0,0 - body 5111326_6618461_-1069601 5111326_6618461_-1069601 kind=MOON orbit=0 radius=1.2042860777026476 starId=-1405460665 frame=false at=-36390,0,29745 - body 5111326_6618461_-1069601 5111326_6618461_-1069601 kind=MOON orbit=0 radius=1.3942527248014411 starId=-1405460665 frame=false at=6387,0,-43736 - body 5111326_6618461_-1069601 5111326_6618461_-1069601 kind=ROGUE_PLANET orbit=0 radius=0.25203985524006844 starId=-1405460665 frame=true at=0,0,0 - body 525712_1170379_4858268 525712_1170379_4858268 kind=MOON orbit=0 radius=1.4063633002353326 starId=-1715483833 frame=false at=203859,0,278576 - body 525712_1170379_4858268 525712_1170379_4858268 kind=MOON orbit=0 radius=1.4846251172749703 starId=-1715483833 frame=false at=64975,0,-181309 - body 525712_1170379_4858268 525712_1170379_4858268 kind=ROGUE_PLANET orbit=0 radius=1.478514845293077 starId=-1715483833 frame=true at=0,0,0 - body 6058391_-999455_4262104 6058391_-999455_4262104 kind=ROGUE_PLANET orbit=0 radius=0.23889752716043286 starId=-354433101 frame=true at=0,0,0 - body 6359032_-1380151_2520214 6359032_-1380151_2520214 kind=MOON orbit=0 radius=0.5435674684313252 starId=-787615481 frame=false at=339354,0,-162838 - body 6359032_-1380151_2520214 6359032_-1380151_2520214 kind=ROGUE_PLANET orbit=0 radius=1.3482517416659634 starId=-787615481 frame=true at=0,0,0 - body 6460860_1396944_-1584535 6460860_1396944_-1584535 kind=MOON orbit=0 radius=0.5938128931086055 starId=-1334375405 frame=false at=175550,0,-162183 - body 6460860_1396944_-1584535 6460860_1396944_-1584535 kind=MOON orbit=0 radius=1.3598734677567275 starId=-1334375405 frame=false at=71739,0,199706 - body 6460860_1396944_-1584535 6460860_1396944_-1584535 kind=ROGUE_PLANET orbit=0 radius=1.0717648155695128 starId=-1334375405 frame=true at=0,0,0 - derived -1923947_-1394043_-2837349 -1923947_-1394043_-2837349 type=ice mass=0.06581115549395339 radius=0.5105195562020988 gravity=25 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=10556 metallicity=1.365986142592694 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2547136_6553750_-2327906 -2547136_6553750_-2327906 type=barren mass=0.6098306725126589 radius=0.9094368622429794 gravity=74 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=22028 metallicity=0.5143935285269323 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3335279_-2608304_4283952 -3335279_-2608304_4283952 type=ice mass=0.9464517177318977 radius=1.052788722587457 gravity=85 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=9265 metallicity=0.5568980851374733 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3347275_169917_-2040789 -3347275_169917_-2040789 type=superearth mass=13.275397753273984 radius=1.9202731970800502 gravity=360 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=14424 metallicity=0.6418218811136412 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3350400_5100916_4798244 -3350400_5100916_4798244 type=ice mass=0.14297778376512693 radius=0.605114195212058 gravity=39 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=24918 metallicity=0.40432658726119813 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -428001_4188003_1035570 -428001_4188003_1035570 type=ice mass=1.1681076894397813 radius=1.1126961933492066 gravity=94 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=11050 metallicity=1.186590387662034 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -428852_-975510_1152659 type=icegiant mass=181.9187945769868 radius=8.628520970273293 gravity=244 pressure=1600 tempK=68 oxygen=false locked=false rings=false rotation=7564 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429309_-975487_1152547 type=gasgiant mass=247.76098517707226 radius=9.868809637033028 gravity=254 pressure=1600 tempK=170 oxygen=false locked=false rings=false rotation=11200 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429373_-975485_1152519 type=icegiant mass=227.78370878102936 radius=9.514604622498513 gravity=252 pressure=1600 tempK=228 oxygen=false locked=false rings=false rotation=9677 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429376_-975486_1152564 type=ice mass=0.011437734978167158 radius=0.3074005137389715 gravity=12 pressure=0 tempK=141 oxygen=false locked=false rings=false rotation=8552 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429390_-975487_1152605 type=barren mass=0.016674658861533066 radius=0.325630568968772 gravity=16 pressure=0 tempK=126 oxygen=false locked=false rings=false rotation=11877 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429399_-975487_1152563 type=lava mass=15.266327328708803 radius=2.031253823812593 gravity=370 pressure=116 tempK=2257 oxygen=false locked=true rings=false rotation=27484 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429401_-975487_1152561 type=greenhouse mass=6.884652355122905 radius=1.7488084514421833 gravity=225 pressure=1600 tempK=844 oxygen=false locked=true rings=false rotation=12787 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429404_-975487_1152562 type=barren mass=0.34239671698596114 radius=0.7162074824220463 gravity=67 pressure=18 tempK=370 oxygen=false locked=true rings=false rotation=16101 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429404_-975487_1152573 type=exotic mass=0.35764448143396327 radius=0.7218413062090878 gravity=69 pressure=54 tempK=238 oxygen=false locked=false rings=false rotation=14846 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429544_-975483_1152487 type=superearth mass=8.032522905559032 radius=1.8057296310075321 gravity=246 pressure=1600 tempK=137 oxygen=false locked=false rings=false rotation=43703 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -429399_-975487_1152563 -429718_-975476_1152699 type=barren mass=0.016085888393307872 radius=0.3279921891899943 gravity=15 pressure=1 tempK=44 oxygen=false locked=false rings=false rotation=7075 metallicity=0.389191529906341 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -565986_1972460_5527035 -565986_1972460_5527035 type=barren mass=0.002875605684610644 radius=0.20510853917918126 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=95817 metallicity=0.47700164567822406 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -810232_3115510_2746902 -810232_3115510_2746902 type=superearth mass=7.360191716771793 radius=1.7208896712788562 gravity=249 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=12953 metallicity=0.4699863784583445 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1713158_-2537384_2172172 1713158_-2537384_2172172 type=ice mass=0.3756785282872785 radius=0.7718606386627891 gravity=63 pressure=0 tempK=31 oxygen=false locked=false rings=false rotation=6696 metallicity=0.6913906038924351 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1737362_-2563157_5310115 1737362_-2563157_5310115 type=barren mass=0.005260174636017469 radius=0.24063193054985418 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=16166 metallicity=1.4267140251012704 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 237817_5592419_4203270 237817_5592419_4203270 type=superearth mass=4.373813703690852 radius=1.4298526622976593 gravity=214 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=8245 metallicity=0.6855545633872082 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2414414_3713625_-2289058 2414414_3713625_-2289058 type=ice mass=0.7485232064127778 radius=0.8921379780825387 gravity=94 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=27515 metallicity=1.3289815536050376 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2528228_-231370_-794020 2528228_-231370_-794020 type=superearth mass=7.061913483941015 radius=1.6503145995487494 gravity=259 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=7871 metallicity=0.7346330420791677 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2663742_1091380_-601497 2663369_1091393_-601424 type=icegiant mass=119.99649062719153 radius=7.200575654829647 gravity=231 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=5780 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2663742_1091380_-601497 2663727_1091381_-601507 type=greenhouse mass=12.676268634867748 radius=2.137014882602409 gravity=278 pressure=1600 tempK=300 oxygen=false locked=false rings=false rotation=44982 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2663742_1091380_-601497 2663742_1091380_-601497 type=lava mass=1.7614751197982401 radius=1.099871592716175 gravity=146 pressure=7 tempK=1809 oxygen=false locked=true rings=false rotation=65128 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2663742_1091380_-601497 2663746_1091380_-601495 type=desert mass=0.22531981189321174 radius=0.654463219883428 gravity=53 pressure=2 tempK=362 oxygen=false locked=true rings=false rotation=63880 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2663742_1091380_-601497 2663756_1091380_-601527 type=gasgiant mass=113.83230272606916 radius=7.03735373269887 gravity=230 pressure=1600 tempK=264 oxygen=false locked=false rings=true rotation=8487 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2663742_1091380_-601497 2663946_1091379_-601618 type=barren mass=0.029767500903840297 radius=0.3669832544972964 gravity=22 pressure=21 tempK=50 oxygen=false locked=false rings=false rotation=63802 metallicity=1.3171108149978235 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2680390_5368406_2491985 2680390_5368406_2491985 type=barren mass=0.13652193362659212 radius=0.6291674918965895 gravity=34 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=87004 metallicity=1.4780569835229094 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2779562_651884_2693855 2779562_651884_2693855 type=ice mass=4.958504054661111 radius=1.5977631514755333 gravity=194 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=14650 metallicity=0.5959706790646394 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3951440_4496837_903895 3951440_4496837_903895 type=barren mass=0.007155848830710717 radius=0.2824811542361627 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=9871 metallicity=0.47319187317369416 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4245760_1904115_3414082 4245760_1904115_3414082 type=ice mass=1.8812269044999095 radius=1.1983557194239915 gravity=131 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=52117 metallicity=1.0092468005284974 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4758850_1796822_6525935 4758850_1796822_6525935 type=ice mass=32.65333761348136 radius=2.430158213292819 gravity=400 pressure=0 tempK=54 oxygen=false locked=false rings=false rotation=14123 metallicity=0.44081078770139615 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4871731_-1898639_-2858414 4871373_-1898655_-2858243 type=barren mass=0.0032792476630666718 radius=0.20276781582524947 gravity=8 pressure=0 tempK=52 oxygen=false locked=false rings=false rotation=33319 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4871731_-1898639_-2858414 4871730_-1898639_-2858397 type=ocean mass=1.5421436119125114 radius=1.1128743239382588 gravity=125 pressure=228 tempK=354 oxygen=true locked=false rings=false rotation=11651 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4871731_-1898639_-2858414 4871731_-1898639_-2858414 type=lava mass=0.06557128550471429 radius=0.49321904660895427 gravity=27 pressure=0 tempK=1003 oxygen=false locked=true rings=false rotation=23896 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4871731_-1898639_-2858414 4871733_-1898639_-2858412 type=barren mass=0.01639213260149288 radius=0.31660185583810646 gravity=16 pressure=0 tempK=512 oxygen=false locked=true rings=false rotation=10423 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4871731_-1898639_-2858414 4871781_-1898639_-2858386 type=exotic mass=1.832121060633507 radius=1.1894637898905132 gravity=129 pressure=1600 tempK=294 oxygen=false locked=false rings=false rotation=94285 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4871731_-1898639_-2858414 4871804_-1898637_-2858342 type=gasgiant mass=93.81910279069727 radius=6.469918942207257 gravity=224 pressure=1600 tempK=201 oxygen=false locked=false rings=true rotation=10743 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4871731_-1898639_-2858414 4871865_-1898612_-2859034 type=superearth mass=6.792729959242305 radius=1.6529479912454907 gravity=249 pressure=1600 tempK=88 oxygen=false locked=false rings=false rotation=27925 metallicity=1.0286974424659112 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5051436_4723973_4799957 5051436_4723973_4799957 type=barren mass=0.014729343074669284 radius=0.3044740134514678 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=15038 metallicity=0.35584698195458186 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5111326_6618461_-1069601 5111326_6618461_-1069601 type=barren mass=0.005438671353740839 radius=0.25203985524006844 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=60332 metallicity=1.2170486304129637 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 525712_1170379_4858268 525712_1170379_4858268 type=ice mass=3.772526885339521 radius=1.478514845293077 gravity=173 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=38853 metallicity=1.1660028429205385 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6058391_-999455_4262104 6058391_-999455_4262104 type=ice mass=0.00400535705780492 radius=0.23889752716043286 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=42845 metallicity=1.3833861048329483 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6359032_-1380151_2520214 6359032_-1380151_2520214 type=superearth mass=3.4010125266906144 radius=1.3482517416659634 gravity=187 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=41051 metallicity=1.5663099662866813 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6460860_1396944_-1584535 6460860_1396944_-1584535 type=ice mass=1.3943441840804145 radius=1.0717648155695128 gravity=121 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=12526 metallicity=0.43936845070542097 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1923947_-1394043_-2837349 id=-1836287757 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless - system -2547136_6553750_-2327906 id=-384211249 kind=ROGUE_PLANET name=PGR--3525313.3525313.-3525313 starless - system -3335279_-2608304_4283952 id=-783595273 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless - system -3347275_169917_-2040789 id=-1080499465 kind=ROGUE_PLANET name=PGR--3525313.0.-3525313 starless - system -3350400_5100916_4798244 id=-513667701 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless - system -428001_4188003_1035570 id=-1929605705 kind=ROGUE_PLANET name=PGR--3525313.3525313.0 starless - system -429399_-975487_1152563 id=-751569025 kind=STAR name=PGS--3525313.-3525313.0 starTemp=70 starSize=1.0282940864562988 - system -565986_1972460_5527035 id=-1194177805 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless - system -810232_3115510_2746902 id=-1645051273 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless - system 1713158_-2537384_2172172 id=-1753285501 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless - system 1737362_-2563157_5310115 id=-1864487113 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless - system 237817_5592419_4203270 id=-1978443773 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless - system 2414414_3713625_-2289058 id=-352841721 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless - system 2528228_-231370_-794020 id=-1916009805 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless - system 2663742_1091380_-601497 id=-824346553 kind=STAR name=PGS-0.0.-3525313 starTemp=70 starSize=0.9003437161445618 - system 2680390_5368406_2491985 id=-1809354021 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless - system 2779562_651884_2693855 id=-342178325 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 3951440_4496837_903895 id=-610246349 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless - system 4245760_1904115_3414082 id=-847248597 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless - system 4758850_1796822_6525935 id=-1218084841 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless - system 4871731_-1898639_-2858414 id=-782061137 kind=STAR name=PGS-3525313.-3525313.-3525313 starTemp=40 starSize=0.7899463772773743 - system 5051436_4723973_4799957 id=-1829532925 kind=ROGUE_PLANET name=PGR-3525313.3525313.3525313 starless - system 5111326_6618461_-1069601 id=-1405460665 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless - system 525712_1170379_4858268 id=-1715483833 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless - system 6058391_-999455_4262104 id=-354433101 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless - system 6359032_-1380151_2520214 id=-787615481 kind=ROGUE_PLANET name=PGR-3525313.-3525313.0 starless - system 6460860_1396944_-1584535 id=-1334375405 kind=ROGUE_PLANET name=PGR-3525313.0.-3525313 starless -seed 8675309 systems=27 - body -1354226_-775980_5896155 -1354226_-775980_5896155 kind=ROGUE_PLANET orbit=0 radius=0.2095823280782168 starId=-245847961 frame=true at=0,0,0 - body -1786692_-493284_1495624 -1786692_-493284_1495624 kind=MOON orbit=0 radius=0.7693291786735446 starId=-1420304977 frame=false at=-36403,0,-3793 - body -1786692_-493284_1495624 -1786692_-493284_1495624 kind=ROGUE_PLANET orbit=0 radius=0.20049653427640265 starId=-1420304977 frame=true at=0,0,0 - body -2263975_2500210_-512704 -2263975_2500210_-512704 kind=ROGUE_PLANET orbit=0 radius=2.3061179305736363 starId=-544380065 frame=true at=0,0,0 - body -2453677_750464_5429323 -2453677_750464_5429323 kind=ROGUE_PLANET orbit=0 radius=0.20615669038861043 starId=-1103945881 frame=true at=0,0,0 - body -2492763_-2673435_-1919668 -2492763_-2673435_-1919668 kind=ROGUE_PLANET orbit=0 radius=0.9387143903262674 starId=-115633053 frame=true at=0,0,0 - body -2783529_5420979_-114688 -2783529_5420979_-114688 kind=ROGUE_PLANET orbit=0 radius=2.0905623514345244 starId=-301293885 frame=true at=0,0,0 - body -3019098_4194427_955152 -3019098_4194427_955152 kind=MOON orbit=0 radius=0.6449988370706554 starId=-203567605 frame=false at=56478,0,20838 - body -3019098_4194427_955152 -3019098_4194427_955152 kind=ROGUE_PLANET orbit=0 radius=0.20200912568214305 starId=-203567605 frame=true at=0,0,0 - body -917361_6648687_4231175 -917361_6648687_4231175 kind=MOON orbit=0 radius=0.3356662908767457 starId=-1415803897 frame=false at=213478,0,188214 - body -917361_6648687_4231175 -917361_6648687_4231175 kind=ROGUE_PLANET orbit=0 radius=2.3937523821350277 starId=-1415803897 frame=true at=0,0,0 - body -987441_2574102_354383 -987441_2574102_354383 kind=MOON orbit=0 radius=1.658573863970683 starId=-1653738961 frame=false at=45197,0,-40966 - body -987441_2574102_354383 -987441_2574102_354383 kind=ROGUE_PLANET orbit=0 radius=0.4498780048941231 starId=-1653738961 frame=true at=0,0,0 - body 1192693_4180823_6715391 1192693_4180823_6715391 kind=MOON orbit=0 radius=0.20399904677206301 starId=-1464588085 frame=false at=16001,0,-45679 - body 1192693_4180823_6715391 1192693_4180823_6715391 kind=ROGUE_PLANET orbit=0 radius=0.23945296389324872 starId=-1464588085 frame=true at=0,0,0 - body 137508_498291_5548492 137508_498291_5548492 kind=ROGUE_PLANET orbit=0 radius=1.2332711328278239 starId=-1629913369 frame=true at=0,0,0 - body 1926001_-2868405_6450292 1926001_-2868405_6450292 kind=ROGUE_PLANET orbit=0 radius=0.6672128988717654 starId=-1724907213 frame=true at=0,0,0 - body 2329541_4966897_-1325776 2329541_4966897_-1325776 kind=STAR orbit=0 radius=0.0 starId=-1372746905 frame=true at=0,0,0 - body 2329541_4966897_-1325776 2329541_4966898_-1325750 kind=MOON orbit=137 radius=0.3693396668206692 starId=-1372746905 frame=false at=-22479,0,-39444 - body 2329541_4966897_-1325776 2329541_4966898_-1325750 kind=MOON orbit=137 radius=0.4330651757074263 starId=-1372746905 frame=false at=-32862,0,-23154 - body 2329541_4966897_-1325776 2329541_4966898_-1325750 kind=PLANET orbit=137 radius=0.4327238813330402 starId=-1372746905 frame=true at=0,0,0 - body 2329541_4966897_-1325776 2329543_4966897_-1325771 kind=MOON orbit=27 radius=0.49281179784880225 starId=-1372746905 frame=false at=119888,0,-105352 - body 2329541_4966897_-1325776 2329543_4966897_-1325771 kind=PLANET orbit=27 radius=0.5664330555906398 starId=-1372746905 frame=true at=0,0,0 - body 2329541_4966897_-1325776 2329543_4966897_-1325774 kind=MOON orbit=14 radius=0.5063668041753813 starId=-1372746905 frame=false at=133101,0,-98527 - body 2329541_4966897_-1325776 2329543_4966897_-1325774 kind=PLANET orbit=14 radius=0.5911736961048453 starId=-1372746905 frame=true at=0,0,0 - body 2329541_4966897_-1325776 2329577_4966896_-1325777 kind=ASTEROID_BELT orbit=192 radius=0.0 starId=-1372746905 frame=true at=0,0,0 - body 2329541_4966897_-1325776 2329605_4966897_-1325785 kind=GAS_GIANT orbit=346 radius=10.17528264925558 starId=-1372746905 frame=true at=0,0,0 - body 2329541_4966897_-1325776 2329605_4966897_-1325785 kind=MOON orbit=346 radius=0.23621311427066893 starId=-1372746905 frame=false at=-2302671,0,-1848760 - body 2329541_4966897_-1325776 2329644_4966893_-1325768 kind=ASTEROID_BELT orbit=553 radius=0.0 starId=-1372746905 frame=true at=0,0,0 - body 2889434_-3209715_-1090932 2883106_-3209715_-1087241 kind=STAR orbit=39178 radius=106.17007929861546 starId=-1616632874 frame=true at=0,0,0 - body 2889434_-3209715_-1090932 2889367_-3209711_-1090865 kind=GAS_GIANT orbit=508 radius=7.359168562793266 starId=-1616632873 frame=true at=0,0,0 - body 2889434_-3209715_-1090932 2889422_-3209715_-1090925 kind=ASTEROID_BELT orbit=72 radius=0.0 starId=-1616632873 frame=true at=0,0,0 - body 2889434_-3209715_-1090932 2889434_-3209715_-1090925 kind=MOON orbit=35 radius=0.20221583652227892 starId=-1616632873 frame=false at=-123674,0,327635 - body 2889434_-3209715_-1090932 2889434_-3209715_-1090925 kind=MOON orbit=35 radius=0.5378447126285323 starId=-1616632873 frame=false at=416020,0,576584 - body 2889434_-3209715_-1090932 2889434_-3209715_-1090925 kind=PLANET orbit=35 radius=2.3898701900773904 starId=-1616632873 frame=true at=0,0,0 - body 2889434_-3209715_-1090932 2889434_-3209715_-1090930 kind=MOON orbit=10 radius=0.21415529303721711 starId=-1616632873 frame=false at=-19511,0,25111 - body 2889434_-3209715_-1090932 2889434_-3209715_-1090930 kind=MOON orbit=10 radius=0.5605312080448812 starId=-1616632873 frame=false at=7333,0,12856 - body 2889434_-3209715_-1090932 2889434_-3209715_-1090930 kind=PLANET orbit=10 radius=0.20001270394003767 starId=-1616632873 frame=true at=0,0,0 - body 2889434_-3209715_-1090932 2889434_-3209715_-1090932 kind=STAR orbit=0 radius=0.0 starId=-1616632873 frame=true at=0,0,0 - body 2889434_-3209715_-1090932 2889455_-3209716_-1090920 kind=GAS_GIANT orbit=131 radius=6.384029999980021 starId=-1616632873 frame=true at=0,0,0 - body 2889434_-3209715_-1090932 2889455_-3209716_-1090920 kind=MOON orbit=131 radius=0.20006308834778394 starId=-1616632873 frame=false at=277681,0,-1682236 - body 2889434_-3209715_-1090932 2889455_-3209716_-1090920 kind=MOON orbit=131 radius=0.39075281887512414 starId=-1616632873 frame=false at=1235464,0,-354073 - body 2889434_-3209715_-1090932 2889556_-3209712_-1091022 kind=ASTEROID_BELT orbit=812 radius=0.0 starId=-1616632873 frame=true at=0,0,0 - body 3182346_-1200494_1223840 3182346_-1200494_1223840 kind=MOON orbit=0 radius=0.7525009834429903 starId=-1628626657 frame=false at=-74042,0,-192877 - body 3182346_-1200494_1223840 3182346_-1200494_1223840 kind=ROGUE_PLANET orbit=0 radius=0.7273830659106353 starId=-1628626657 frame=true at=0,0,0 - body 3260852_6822578_1746102 3260852_6822578_1746102 kind=ROGUE_PLANET orbit=0 radius=1.2795628404660984 starId=-24266345 frame=true at=0,0,0 - body 3723419_2156869_-1335565 3723396_2156869_-1335603 kind=GAS_GIANT orbit=239 radius=7.725594531511088 starId=-392697649 frame=true at=0,0,0 - body 3723419_2156869_-1335565 3723396_2156869_-1335603 kind=MOON orbit=239 radius=0.20291586860664781 starId=-392697649 frame=false at=-1416202,0,597278 - body 3723419_2156869_-1335565 3723396_2156869_-1335603 kind=MOON orbit=239 radius=0.7278912456238515 starId=-392697649 frame=false at=1381989,0,-1257091 - body 3723419_2156869_-1335565 3723397_2156870_-1335553 kind=ASTEROID_BELT orbit=132 radius=0.0 starId=-392697649 frame=true at=0,0,0 - body 3723419_2156869_-1335565 3723418_2156868_-1335551 kind=PLANET orbit=73 radius=1.633148040692367 starId=-392697649 frame=true at=0,0,0 - body 3723419_2156869_-1335565 3723419_2156869_-1335565 kind=STAR orbit=0 radius=0.0 starId=-392697649 frame=true at=0,0,0 - body 3723419_2156869_-1335565 3723422_2156869_-1335564 kind=MOON orbit=19 radius=0.34710916896983884 starId=-392697649 frame=false at=-12446,0,43661 - body 3723419_2156869_-1335565 3723422_2156869_-1335564 kind=PLANET orbit=19 radius=0.20888798108361883 starId=-392697649 frame=true at=0,0,0 - body 3723419_2156869_-1335565 3723441_2156872_-1335633 kind=ASTEROID_BELT orbit=382 radius=0.0 starId=-392697649 frame=true at=0,0,0 - body 3822552_4765867_-2982992 3822552_4765867_-2982992 kind=ROGUE_PLANET orbit=0 radius=0.7075016622871881 starId=-646721517 frame=true at=0,0,0 - body 3927952_4810457_3631655 3924557_4810385_3631323 kind=GAS_GIANT orbit=18248 radius=7.532755154412924 starId=-771612361 frame=true at=0,0,0 - body 3927952_4810457_3631655 3924557_4810385_3631323 kind=MOON orbit=18248 radius=0.2398248770474635 starId=-771612361 frame=false at=773716,0,-992185 - body 3927952_4810457_3631655 3924557_4810385_3631323 kind=MOON orbit=18248 radius=0.3250924220967756 starId=-771612361 frame=false at=-1724671,0,62410 - body 3927952_4810457_3631655 3926914_4810506_3631484 kind=PLANET orbit=5631 radius=1.4188668287815471 starId=-771612361 frame=true at=0,0,0 - body 3927952_4810457_3631655 3927727_4810461_3631449 kind=GAS_GIANT orbit=1632 radius=4.830052167417934 starId=-771612361 frame=true at=0,0,0 - body 3927952_4810457_3631655 3927882_4810463_3631809 kind=ASTEROID_BELT orbit=906 radius=0.0 starId=-771612361 frame=true at=0,0,0 - body 3927952_4810457_3631655 3927952_4810457_3631655 kind=STAR orbit=0 radius=0.0 starId=-771612361 frame=true at=0,0,0 - body 3927952_4810457_3631655 3927997_4810459_3631662 kind=MOON orbit=241 radius=0.49560078394410945 starId=-771612361 frame=false at=-95660,0,-587665 - body 3927952_4810457_3631655 3927997_4810459_3631662 kind=PLANET orbit=241 radius=2.078438494405952 starId=-771612361 frame=true at=0,0,0 - body 3927952_4810457_3631655 3933209_4810411_3630182 kind=ASTEROID_BELT orbit=29196 radius=0.0 starId=-771612361 frame=true at=0,0,0 - body 3991853_-3409879_1695966 3991812_-3409882_1696009 kind=PLANET orbit=320 radius=0.2000049072546915 starId=-532811557 frame=true at=0,0,0 - body 3991853_-3409879_1695966 3991834_-3409881_1695997 kind=PLANET orbit=194 radius=2.428323573235008 starId=-532811557 frame=true at=0,0,0 - body 3991853_-3409879_1695966 3991853_-3409879_1695966 kind=STAR orbit=0 radius=0.0 starId=-532811557 frame=true at=0,0,0 - body 3991853_-3409879_1695966 3991853_-3409879_1695970 kind=ASTEROID_BELT orbit=20 radius=0.0 starId=-532811557 frame=true at=0,0,0 - body 3991853_-3409879_1695966 3991854_-3409879_1695962 kind=PLANET orbit=20 radius=0.22033987090436358 starId=-532811557 frame=true at=0,0,0 - body 3991853_-3409879_1695966 3991854_-3409879_1695966 kind=PLANET orbit=8 radius=0.22662800708790426 starId=-532811557 frame=true at=0,0,0 - body 3991853_-3409879_1695966 3991859_-3409879_1695970 kind=GAS_GIANT orbit=36 radius=9.047143938187649 starId=-532811557 frame=true at=0,0,0 - body 3991853_-3409879_1695966 3991868_-3409878_1695958 kind=MOON orbit=93 radius=0.22995041668892738 starId=-532811557 frame=false at=174171,0,47757 - body 3991853_-3409879_1695966 3991868_-3409878_1695958 kind=MOON orbit=93 radius=0.6330754458586816 starId=-532811557 frame=false at=-249338,0,168616 - body 3991853_-3409879_1695966 3991868_-3409878_1695958 kind=PLANET orbit=93 radius=1.2416552219403456 starId=-532811557 frame=true at=0,0,0 - body 3991853_-3409879_1695966 3991928_-3409882_1696026 kind=ASTEROID_BELT orbit=512 radius=0.0 starId=-532811557 frame=true at=0,0,0 - body 4713087_-1117360_-531483 4713087_-1117360_-531483 kind=ROGUE_PLANET orbit=0 radius=0.5492908503901086 starId=-1273646913 frame=true at=0,0,0 - body 5671190_876631_2238047 5671190_876631_2238047 kind=MOON orbit=0 radius=1.8347587972498054 starId=-1812954729 frame=false at=-59469,0,53512 - body 5671190_876631_2238047 5671190_876631_2238047 kind=ROGUE_PLANET orbit=0 radius=0.3623353932076434 starId=-1812954729 frame=true at=0,0,0 - body 6440405_-2011426_5750662 6440405_-2011426_5750662 kind=ROGUE_PLANET orbit=0 radius=2.1711386375337 starId=-929483845 frame=true at=0,0,0 - body 6464859_1421762_4096630 6464859_1421762_4096630 kind=MOON orbit=0 radius=0.739675313296466 starId=-1373082229 frame=false at=200198,0,-248615 - body 6464859_1421762_4096630 6464859_1421762_4096630 kind=ROGUE_PLANET orbit=0 radius=1.3688524064028083 starId=-1373082229 frame=true at=0,0,0 - body 662972_1432839_1639281 662972_1432839_1639281 kind=MOON orbit=0 radius=0.30956397773965205 starId=-576770217 frame=false at=70016,0,240204 - body 662972_1432839_1639281 662972_1432839_1639281 kind=ROGUE_PLANET orbit=0 radius=1.8066540452223296 starId=-576770217 frame=true at=0,0,0 - body 6924773_4236388_1837031 6924773_4236388_1837031 kind=ROGUE_PLANET orbit=0 radius=0.25767849632181145 starId=-1211781741 frame=true at=0,0,0 - body 793862_2575944_-859446 793862_2575944_-859446 kind=MOON orbit=0 radius=1.0287217786888536 starId=-508002589 frame=false at=88065,0,-17567 - body 793862_2575944_-859446 793862_2575944_-859446 kind=ROGUE_PLANET orbit=0 radius=0.5129142760947367 starId=-508002589 frame=true at=0,0,0 - derived -1354226_-775980_5896155 -1354226_-775980_5896155 type=ice mass=0.0031285574176620656 radius=0.2095823280782168 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=9265 metallicity=1.5347243409224713 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1786692_-493284_1495624 -1786692_-493284_1495624 type=barren mass=0.0023689048914081463 radius=0.20049653427640265 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=10746 metallicity=0.8821677094649482 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2263975_2500210_-512704 -2263975_2500210_-512704 type=ice mass=26.29840821160823 radius=2.3061179305736363 gravity=400 pressure=0 tempK=52 oxygen=false locked=false rings=false rotation=6300 metallicity=0.6192761496322858 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2453677_750464_5429323 -2453677_750464_5429323 type=ice mass=0.002322737197980196 radius=0.20615669038861043 gravity=5 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=28078 metallicity=0.9579572043963579 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2492763_-2673435_-1919668 -2492763_-2673435_-1919668 type=ice mass=0.8524265034311743 radius=0.9387143903262674 gravity=97 pressure=0 tempK=35 oxygen=false locked=false rings=true rotation=44275 metallicity=0.9064800766555258 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2783529_5420979_-114688 -2783529_5420979_-114688 type=ice mass=16.178098163884066 radius=2.0905623514345244 gravity=370 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=11736 metallicity=0.782820294838597 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3019098_4194427_955152 -3019098_4194427_955152 type=barren mass=0.0025857474895548105 radius=0.20200912568214305 gravity=6 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=34059 metallicity=0.35261620615877204 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -917361_6648687_4231175 -917361_6648687_4231175 type=ice mass=21.61810436895705 radius=2.3937523821350277 gravity=377 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=16371 metallicity=0.41798816435242464 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -987441_2574102_354383 -987441_2574102_354383 type=barren mass=0.04936332087010038 radius=0.4498780048941231 gravity=24 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=29600 metallicity=0.879350259899443 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1192693_4180823_6715391 1192693_4180823_6715391 type=ice mass=0.0057940503067513555 radius=0.23945296389324872 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=76651 metallicity=1.0762488182169192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 137508_498291_5548492 137508_498291_5548492 type=superearth mass=2.64578834872378 radius=1.2332711328278239 gravity=174 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=68817 metallicity=0.6373928080636878 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1926001_-2868405_6450292 1926001_-2868405_6450292 type=barren mass=0.27026928160409147 radius=0.6672128988717654 gravity=61 pressure=0 tempK=31 oxygen=false locked=false rings=false rotation=26219 metallicity=1.0236486544157484 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2329541_4966897_-1325776 2329541_4966897_-1325776 type=lava mass=10.97417172944396 radius=1.8898232597957598 gravity=307 pressure=244 tempK=1528 oxygen=false locked=true rings=false rotation=9563 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2329541_4966897_-1325776 2329541_4966898_-1325750 type=barren mass=0.049531639951755274 radius=0.4327238813330402 gravity=26 pressure=13 tempK=92 oxygen=false locked=false rings=false rotation=23245 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2329541_4966897_-1325776 2329543_4966897_-1325771 type=barren mass=0.09629020722034799 radius=0.5664330555906398 gravity=30 pressure=1 tempK=208 oxygen=false locked=true rings=false rotation=25004 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2329541_4966897_-1325776 2329543_4966897_-1325774 type=desert mass=0.11412324272634686 radius=0.5911736961048453 gravity=33 pressure=1 tempK=272 oxygen=false locked=true rings=false rotation=23746 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2329541_4966897_-1325776 2329577_4966896_-1325777 type=gasgiant mass=239.81712962466773 radius=9.729968087372685 gravity=253 pressure=1600 tempK=152 oxygen=false locked=false rings=true rotation=12847 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2329541_4966897_-1325776 2329605_4966897_-1325785 type=icegiant mass=265.8158372253331 radius=10.17528264925558 gravity=257 pressure=1600 tempK=113 oxygen=false locked=false rings=true rotation=5832 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2329541_4966897_-1325776 2329644_4966893_-1325768 type=ice mass=0.0654470383830132 radius=0.48744795473595487 gravity=28 pressure=30 tempK=37 oxygen=false locked=false rings=false rotation=13755 metallicity=0.6758196714019941 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2889434_-3209715_-1090932 2883106_-3209715_-1087241 type=ice mass=4.9461644443979065 radius=1.5223619472849548 gravity=213 pressure=1600 tempK=15 oxygen=false locked=false rings=false rotation=42163 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2889434_-3209715_-1090932 2889367_-3209711_-1090865 type=gasgiant mass=126.16243733242267 radius=7.359168562793266 gravity=233 pressure=1600 tempK=92 oxygen=false locked=false rings=true rotation=14139 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2889434_-3209715_-1090932 2889422_-3209715_-1090925 type=ice mass=0.04363744413674012 radius=0.41128823750065546 gravity=26 pressure=3 tempK=103 oxygen=false locked=false rings=false rotation=32220 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2889434_-3209715_-1090932 2889434_-3209715_-1090925 type=greenhouse mass=26.407567764613358 radius=2.3898701900773904 gravity=400 pressure=1600 tempK=296 oxygen=false locked=true rings=false rotation=57780 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2889434_-3209715_-1090932 2889434_-3209715_-1090930 type=barren mass=0.002681979042407941 radius=0.20001270394003767 gravity=7 pressure=0 tempK=337 oxygen=false locked=true rings=false rotation=49490 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2889434_-3209715_-1090932 2889434_-3209715_-1090932 type=barren mass=0.002694205359879519 radius=0.20039817314943498 gravity=7 pressure=0 tempK=1068 oxygen=false locked=true rings=false rotation=50226 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2889434_-3209715_-1090932 2889455_-3209716_-1090920 type=gasgiant mass=90.9792302923874 radius=6.384029999980021 gravity=223 pressure=1600 tempK=182 oxygen=false locked=false rings=true rotation=7547 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2889434_-3209715_-1090932 2889556_-3209712_-1091022 type=barren mass=0.011631918397034965 radius=0.2959512062908492 gravity=13 pressure=1 tempK=37 oxygen=false locked=false rings=false rotation=54613 metallicity=1.1093691041245042 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3182346_-1200494_1223840 3182346_-1200494_1223840 type=ice mass=0.2644150271083819 radius=0.7273830659106353 gravity=50 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=7675 metallicity=0.5518987982242867 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3260852_6822578_1746102 3260852_6822578_1746102 type=ice mass=2.4359543310779155 radius=1.2795628404660984 gravity=149 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=43959 metallicity=1.0755213177926986 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3723419_2156869_-1335565 3723396_2156869_-1335603 type=gasgiant mass=141.0806134433791 radius=7.725594531511088 gravity=236 pressure=1600 tempK=127 oxygen=false locked=false rings=true rotation=12816 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3723419_2156869_-1335565 3723397_2156870_-1335553 type=ice mass=23.366603194962387 radius=2.319630111342125 gravity=400 pressure=1600 tempK=162 oxygen=false locked=false rings=false rotation=6657 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3723419_2156869_-1335565 3723418_2156868_-1335551 type=superearth mass=7.072302498375005 radius=1.633148040692367 gravity=265 pressure=1600 tempK=250 oxygen=false locked=false rings=false rotation=20174 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3723419_2156869_-1335565 3723419_2156869_-1335565 type=barren mass=0.0030701447389803204 radius=0.20847545067095652 gravity=7 pressure=0 tempK=1008 oxygen=false locked=true rings=false rotation=46531 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3723419_2156869_-1335565 3723422_2156869_-1335564 type=barren mass=0.002966178789951872 radius=0.20888798108361883 gravity=7 pressure=0 tempK=231 oxygen=false locked=true rings=false rotation=83304 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3723419_2156869_-1335565 3723441_2156872_-1335633 type=superearth mass=13.304249276732389 radius=2.0003350115729988 gravity=332 pressure=1600 tempK=109 oxygen=false locked=false rings=false rotation=26917 metallicity=1.3077693371543293 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3822552_4765867_-2982992 3822552_4765867_-2982992 type=barren mass=0.2159765047044719 radius=0.7075016622871881 gravity=43 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=17181 metallicity=1.387776541310441 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3927952_4810457_3631655 3924557_4810385_3631323 type=gasgiant mass=133.11218187197326 radius=7.532755154412924 gravity=235 pressure=1600 tempK=106 oxygen=false locked=false rings=true rotation=12677 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3927952_4810457_3631655 3926914_4810506_3631484 type=ice mass=4.26622428294551 radius=1.4188668287815471 gravity=212 pressure=1600 tempK=182 oxygen=false locked=false rings=false rotation=7788 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3927952_4810457_3631655 3927727_4810461_3631449 type=gasgiant mass=47.89753168313653 radius=4.830052167417934 gravity=205 pressure=1600 tempK=357 oxygen=false locked=false rings=true rotation=13722 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3927952_4810457_3631655 3927882_4810463_3631809 type=barren mass=0.00426452682722637 radius=0.2179148571877407 gravity=9 pressure=0 tempK=245 oxygen=false locked=false rings=false rotation=16477 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3927952_4810457_3631655 3927952_4810457_3631655 type=unclassified mass=0.07525681150547835 radius=0.47039909734528434 gravity=34 pressure=0 tempK=6985 oxygen=false locked=true rings=false rotation=37231 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3927952_4810457_3631655 3927997_4810459_3631662 type=lava mass=16.96892069408671 radius=2.078438494405952 gravity=393 pressure=1600 tempK=1078 oxygen=false locked=false rings=false rotation=61020 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3927952_4810457_3631655 3933209_4810411_3630182 type=gasgiant mass=24.08121298114381 radius=3.581876560847876 gravity=188 pressure=1600 tempK=84 oxygen=false locked=false rings=true rotation=6252 metallicity=0.5594497827314833 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3991853_-3409879_1695966 3991812_-3409882_1696009 type=ice mass=0.0027299710707084555 radius=0.2000049072546915 gravity=7 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=26118 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3991853_-3409879_1695966 3991834_-3409881_1695997 type=ice mass=22.304600037860514 radius=2.428323573235008 gravity=378 pressure=1600 tempK=123 oxygen=false locked=false rings=false rotation=14789 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3991853_-3409879_1695966 3991853_-3409879_1695966 type=lava mass=2.8230307532429397 radius=1.362518247910563 gravity=152 pressure=66 tempK=948 oxygen=false locked=true rings=false rotation=6325 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3991853_-3409879_1695966 3991853_-3409879_1695970 type=exotic mass=0.7368209632598757 radius=0.9316990840993771 gravity=85 pressure=239 tempK=274 oxygen=false locked=true rings=false rotation=82320 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3991853_-3409879_1695966 3991854_-3409879_1695962 type=barren mass=0.0036750378630961253 radius=0.22033987090436358 gravity=8 pressure=0 tempK=208 oxygen=false locked=true rings=false rotation=6613 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3991853_-3409879_1695966 3991854_-3409879_1695966 type=barren mass=0.004100190374783057 radius=0.22662800708790426 gravity=8 pressure=0 tempK=328 oxygen=false locked=true rings=false rotation=6811 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3991853_-3409879_1695966 3991859_-3409879_1695970 type=gasgiant mass=202.86185569294582 radius=9.047143938187649 gravity=248 pressure=1600 tempK=302 oxygen=false locked=false rings=false rotation=6288 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3991853_-3409879_1695966 3991868_-3409878_1695958 type=ice mass=1.9070388107758682 radius=1.2416552219403456 gravity=124 pressure=1600 tempK=178 oxygen=false locked=false rings=false rotation=6048 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3991853_-3409879_1695966 3991928_-3409882_1696026 type=barren mass=0.062140444967901935 radius=0.48401663040802284 gravity=27 pressure=13 tempK=41 oxygen=false locked=false rings=false rotation=63759 metallicity=1.2783804551321944 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4713087_-1117360_-531483 4713087_-1117360_-531483 type=barren mass=0.1246751408394732 radius=0.5492908503901086 gravity=41 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=42693 metallicity=0.39650211260948054 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5671190_876631_2238047 5671190_876631_2238047 type=ice mass=0.023687324890898958 radius=0.3623353932076434 gravity=18 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=24681 metallicity=0.48458331719113257 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6440405_-2011426_5750662 6440405_-2011426_5750662 type=superearth mass=13.786999315655324 radius=2.1711386375337 gravity=292 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=16921 metallicity=1.3836626361029771 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6464859_1421762_4096630 6464859_1421762_4096630 type=ice mass=3.7177160066414987 radius=1.3688524064028083 gravity=198 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=18789 metallicity=0.6218013164273715 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 662972_1432839_1639281 662972_1432839_1639281 type=ice mass=10.857475966415414 radius=1.8066540452223296 gravity=333 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=61901 metallicity=0.6395580353459209 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6924773_4236388_1837031 6924773_4236388_1837031 type=ice mass=0.005876345400979748 radius=0.25767849632181145 gravity=9 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=62110 metallicity=0.5876410185903691 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 793862_2575944_-859446 793862_2575944_-859446 type=ice mass=0.08539001171147081 radius=0.5129142760947367 gravity=32 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=14623 metallicity=0.7867155683259348 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1354226_-775980_5896155 id=-245847961 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless - system -1786692_-493284_1495624 id=-1420304977 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless - system -2263975_2500210_-512704 id=-544380065 kind=ROGUE_PLANET name=PGR--3525313.0.-3525313 starless - system -2453677_750464_5429323 id=-1103945881 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless - system -2492763_-2673435_-1919668 id=-115633053 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless - system -2783529_5420979_-114688 id=-301293885 kind=ROGUE_PLANET name=PGR--3525313.3525313.-3525313 starless - system -3019098_4194427_955152 id=-203567605 kind=ROGUE_PLANET name=PGR--3525313.3525313.0 starless - system -917361_6648687_4231175 id=-1415803897 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless - system -987441_2574102_354383 id=-1653738961 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless - system 1192693_4180823_6715391 id=-1464588085 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless - system 137508_498291_5548492 id=-1629913369 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless - system 1926001_-2868405_6450292 id=-1724907213 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless - system 2329541_4966897_-1325776 id=-1372746905 kind=STAR name=PGS-0.3525313.-3525313 starTemp=40 starSize=0.9957200884819031 - system 2889434_-3209715_-1090932 id=-1616632873 kind=STAR name=PGS-0.-3525313.-3525313 starTemp=40 starSize=0.972520649433136 - system 3182346_-1200494_1223840 id=-1628626657 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless - system 3260852_6822578_1746102 id=-24266345 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless - system 3723419_2156869_-1335565 id=-392697649 kind=STAR name=PGS-3525313.0.-3525313 starTemp=40 starSize=0.8668519854545593 - system 3822552_4765867_-2982992 id=-646721517 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless - system 3927952_4810457_3631655 id=-771612361 kind=STAR name=PGS-3525313.3525313.3525313 starTemp=220 starSize=1.54029381275177 - system 3991853_-3409879_1695966 id=-532811557 kind=STAR name=PGS-3525313.-3525313.0 starTemp=40 starSize=0.7370571494102478 - system 4713087_-1117360_-531483 id=-1273646913 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless - system 5671190_876631_2238047 id=-1812954729 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless - system 6440405_-2011426_5750662 id=-929483845 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless - system 6464859_1421762_4096630 id=-1373082229 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless - system 662972_1432839_1639281 id=-576770217 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 6924773_4236388_1837031 id=-1211781741 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless - system 793862_2575944_-859446 id=-508002589 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless -seed -1 systems=27 - body -1080086_1555356_4409113 -1080086_1555356_4409113 kind=MOON orbit=0 radius=0.4512046985653989 starId=-1337008461 frame=false at=470919,0,-377582 - body -1080086_1555356_4409113 -1080086_1555356_4409113 kind=MOON orbit=0 radius=8.82444722408132 starId=-1337008461 frame=false at=-336265,0,-129669 - body -1080086_1555356_4409113 -1080086_1555356_4409113 kind=ROGUE_PLANET orbit=0 radius=2.038268124956373 starId=-1337008461 frame=true at=0,0,0 - body -1834664_1670352_-1563056 -1834341_1670330_-1563572 kind=PLANET orbit=3256 radius=1.1181559977969782 starId=-1729416869 frame=true at=0,0,0 - body -1834664_1670352_-1563056 -1834603_1670354_-1563099 kind=GAS_GIANT orbit=399 radius=5.676685494197384 starId=-1729416869 frame=true at=0,0,0 - body -1834664_1670352_-1563056 -1834603_1670354_-1563099 kind=MOON orbit=399 radius=0.35039969689273204 starId=-1729416869 frame=false at=1594260,0,688054 - body -1834664_1670352_-1563056 -1834656_1670351_-1563015 kind=ASTEROID_BELT orbit=221 radius=0.0 starId=-1729416869 frame=true at=0,0,0 - body -1834664_1670352_-1563056 -1834664_1670352_-1563056 kind=STAR orbit=0 radius=0.0 starId=-1729416869 frame=true at=0,0,0 - body -1834664_1670352_-1563056 -1834665_1670352_-1563062 kind=MOON orbit=34 radius=0.29895177745722334 starId=-1729416869 frame=false at=-50977,0,-32767 - body -1834664_1670352_-1563056 -1834665_1670352_-1563062 kind=PLANET orbit=34 radius=0.217838595089208 starId=-1729416869 frame=true at=0,0,0 - body -1834664_1670352_-1563056 -1834674_1670352_-1563044 kind=PLANET orbit=84 radius=0.2706016219811198 starId=-1729416869 frame=true at=0,0,0 - body -1834664_1670352_-1563056 -1834697_1670352_-1563082 kind=MOON orbit=225 radius=0.5956547429113542 starId=-1729416869 frame=false at=120313,0,33908 - body -1834664_1670352_-1563056 -1834697_1670352_-1563082 kind=MOON orbit=225 radius=0.7447388998696458 starId=-1729416869 frame=false at=-228355,0,-212304 - body -1834664_1670352_-1563056 -1834697_1670352_-1563082 kind=PLANET orbit=225 radius=1.5301172792246798 starId=-1729416869 frame=true at=0,0,0 - body -1834664_1670352_-1563056 -1834844_1670355_-1562989 kind=MOON orbit=1027 radius=0.292506707547793 starId=-1729416869 frame=false at=-48718,0,38024 - body -1834664_1670352_-1563056 -1834844_1670355_-1562989 kind=MOON orbit=1027 radius=0.3566389214674198 starId=-1729416869 frame=false at=63702,0,-41449 - body -1834664_1670352_-1563056 -1834844_1670355_-1562989 kind=PLANET orbit=1027 radius=0.4169178260700219 starId=-1729416869 frame=true at=0,0,0 - body -1834664_1670352_-1563056 -1835637_1670333_-1563103 kind=ASTEROID_BELT orbit=5209 radius=0.0 starId=-1729416869 frame=true at=0,0,0 - body -2208092_-1790276_1685270 -2208092_-1790276_1685270 kind=ROGUE_PLANET orbit=0 radius=0.2385909118845583 starId=-1765959065 frame=true at=0,0,0 - body -2290821_1403075_856935 -2290821_1403075_856935 kind=MOON orbit=0 radius=1.3548125810973575 starId=-1992245269 frame=false at=-78270,0,-90432 - body -2290821_1403075_856935 -2290821_1403075_856935 kind=ROGUE_PLANET orbit=0 radius=0.6825852386298956 starId=-1992245269 frame=true at=0,0,0 - body -3014847_-1420592_5288623 -3014847_-1420592_5288623 kind=ROGUE_PLANET orbit=0 radius=0.2259652104199226 starId=-794196001 frame=true at=0,0,0 - body -3374315_4976731_3972240 -3374315_4976731_3972240 kind=MOON orbit=0 radius=0.4649359539187697 starId=-1160817649 frame=false at=-20116,0,81144 - body -3374315_4976731_3972240 -3374315_4976731_3972240 kind=ROGUE_PLANET orbit=0 radius=1.257168129941409 starId=-1160817649 frame=true at=0,0,0 - body -436060_-2366191_-949510 -435942_-2366191_-949421 kind=STAR orbit=791 radius=91.17783525288105 starId=-498839402 frame=true at=0,0,0 - body -436060_-2366191_-949510 -436000_-2366193_-949507 kind=ASTEROID_BELT orbit=320 radius=0.0 starId=-498839401 frame=true at=0,0,0 - body -436060_-2366191_-949510 -436025_-2366191_-949523 kind=PLANET orbit=200 radius=1.9765339194370526 starId=-498839401 frame=true at=0,0,0 - body -436060_-2366191_-949510 -436055_-2366191_-949514 kind=PLANET orbit=34 radius=0.34512975441876653 starId=-498839401 frame=true at=0,0,0 - body -436060_-2366191_-949510 -436060_-2366191_-949510 kind=STAR orbit=0 radius=0.0 starId=-498839401 frame=true at=0,0,0 - body -436060_-2366191_-949510 -436062_-2366191_-949508 kind=PLANET orbit=13 radius=0.98242013651816 starId=-498839401 frame=true at=0,0,0 - body -436060_-2366191_-949510 -436065_-2366191_-949500 kind=PLANET orbit=62 radius=1.334015205340586 starId=-498839401 frame=true at=0,0,0 - body -528291_6799917_1589264 -528281_6799917_1589255 kind=MOON orbit=70 radius=0.30983974710458184 starId=-1828554265 frame=false at=-210799,0,96410 - body -528291_6799917_1589264 -528281_6799917_1589255 kind=PLANET orbit=70 radius=1.549060693570877 starId=-1828554265 frame=true at=0,0,0 - body -528291_6799917_1589264 -528289_6799918_1589188 kind=MOON orbit=406 radius=0.2770804126829593 starId=-1828554265 frame=false at=151276,0,-77120 - body -528291_6799917_1589264 -528289_6799918_1589188 kind=MOON orbit=406 radius=0.5931675694487508 starId=-1828554265 frame=false at=-84044,0,-7748 - body -528291_6799917_1589264 -528289_6799918_1589188 kind=PLANET orbit=406 radius=1.2598310614277537 starId=-1828554265 frame=true at=0,0,0 - body -528291_6799917_1589264 -528291_6799917_1589264 kind=STAR orbit=0 radius=0.0 starId=-1828554265 frame=true at=0,0,0 - body -528291_6799917_1589264 -528415_6799932_1589887 kind=MOON orbit=3397 radius=0.21455561212610527 starId=-1828554265 frame=false at=199862,0,57613 - body -528291_6799917_1589264 -528415_6799932_1589887 kind=MOON orbit=3397 radius=0.2404223211669318 starId=-1828554265 frame=false at=445956,0,-71171 - body -528291_6799917_1589264 -528415_6799932_1589887 kind=PLANET orbit=3397 radius=1.9904254713501455 starId=-1828554265 frame=true at=0,0,0 - body -528291_6799917_1589264 -529048_6799943_1588586 kind=ASTEROID_BELT orbit=5435 radius=0.0 starId=-1828554265 frame=true at=0,0,0 - body -935767_4549081_-1731362 -935767_4549081_-1731362 kind=MOON orbit=0 radius=0.22563842767321993 starId=-1358211061 frame=false at=-7718,0,-51222 - body -935767_4549081_-1731362 -935767_4549081_-1731362 kind=ROGUE_PLANET orbit=0 radius=0.21212769702860157 starId=-1358211061 frame=true at=0,0,0 - body 1683940_-183048_-1702546 1683940_-183048_-1702546 kind=ROGUE_PLANET orbit=0 radius=0.28622983833858173 starId=-167956389 frame=true at=0,0,0 - body 1920315_1641073_-2834001 1920315_1641073_-2834001 kind=ROGUE_PLANET orbit=0 radius=0.5693690184380397 starId=-1131707005 frame=true at=0,0,0 - body 233560_2922383_5010954 233560_2922383_5010954 kind=MOON orbit=0 radius=0.3002036042196137 starId=-1390378301 frame=false at=-396679,0,-164931 - body 233560_2922383_5010954 233560_2922383_5010954 kind=ROGUE_PLANET orbit=0 radius=1.4350700127224734 starId=-1390378301 frame=true at=0,0,0 - body 236691_5203788_1778447 236688_5203788_1778436 kind=STAR orbit=59 radius=92.83249720394612 starId=-1521889894 frame=true at=0,0,0 - body 236691_5203788_1778447 236690_5203788_1778446 kind=MOON orbit=8 radius=0.261912143918765 starId=-1521889893 frame=false at=-49373,0,-157230 - body 236691_5203788_1778447 236690_5203788_1778446 kind=PLANET orbit=8 radius=1.5952759531425913 starId=-1521889893 frame=true at=0,0,0 - body 236691_5203788_1778447 236691_5203788_1778447 kind=STAR orbit=0 radius=0.0 starId=-1521889893 frame=true at=0,0,0 - body 236691_5203788_1778447 236693_5203788_1778447 kind=ASTEROID_BELT orbit=12 radius=0.0 starId=-1521889893 frame=true at=0,0,0 - body 236691_5203788_1778447 236832_5203788_1778492 kind=STAR orbit=789 radius=68.7155103546381 starId=-1521889895 frame=true at=0,0,0 - body 2374829_-1121220_6754955 2369207_-1121220_6758455 kind=STAR orbit=35417 radius=75.04819331288338 starId=-1897301650 frame=true at=0,0,0 - body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=GAS_GIANT orbit=319 radius=3.553184930957361 starId=-1897301649 frame=true at=0,0,0 - body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=MOON orbit=319 radius=0.5595779650053618 starId=-1897301649 frame=false at=369129,0,397964 - body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=MOON orbit=319 radius=0.674835009037887 starId=-1897301649 frame=false at=724610,0,570432 - body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=MOON orbit=319 radius=0.6917501847168781 starId=-1897301649 frame=false at=-511342,0,562524 - body 2374829_-1121220_6754955 2374772_-1121218_6754973 kind=MOON orbit=319 radius=0.7215686369589247 starId=-1897301649 frame=false at=978801,0,19754 - body 2374829_-1121220_6754955 2374790_-1121224_6755042 kind=ASTEROID_BELT orbit=510 radius=0.0 starId=-1897301649 frame=true at=0,0,0 - body 2374829_-1121220_6754955 2374827_-1121220_6754949 kind=ASTEROID_BELT orbit=33 radius=0.0 starId=-1897301649 frame=true at=0,0,0 - body 2374829_-1121220_6754955 2374829_-1121220_6754955 kind=STAR orbit=0 radius=0.0 starId=-1897301649 frame=true at=0,0,0 - body 2374829_-1121220_6754955 2374830_-1121220_6754957 kind=PLANET orbit=11 radius=0.2038047543227626 starId=-1897301649 frame=true at=0,0,0 - body 2374829_-1121220_6754955 2374835_-1121220_6754946 kind=GAS_GIANT orbit=61 radius=10.39003058667359 starId=-1897301649 frame=true at=0,0,0 - body 2374829_-1121220_6754955 2374835_-1121220_6754946 kind=MOON orbit=61 radius=0.24251796210681933 starId=-1897301649 frame=false at=263909,0,-694979 - body 2374829_-1121220_6754955 2374835_-1121220_6754946 kind=MOON orbit=61 radius=0.2844468544441377 starId=-1897301649 frame=false at=1902683,0,-1123445 - body 2374829_-1121220_6754955 2374835_-1121220_6754946 kind=MOON orbit=61 radius=0.5393710813599364 starId=-1897301649 frame=false at=-1299806,0,-706072 - body 3353092_-2509636_325419 3353092_-2509636_325419 kind=ROGUE_PLANET orbit=0 radius=0.5025877735087989 starId=-1078102009 frame=true at=0,0,0 - body 3392849_5972712_-904179 3392849_5972712_-904179 kind=MOON orbit=0 radius=0.34153682693735343 starId=-1470468277 frame=false at=323,0,36399 - body 3392849_5972712_-904179 3392849_5972712_-904179 kind=ROGUE_PLANET orbit=0 radius=0.27812854586282826 starId=-1470468277 frame=true at=0,0,0 - body 4690385_6611748_3834194 4690385_6611748_3834194 kind=MOON orbit=0 radius=1.8041026325046543 starId=-284738901 frame=false at=-4009,0,-37185 - body 4690385_6611748_3834194 4690385_6611748_3834194 kind=ROGUE_PLANET orbit=0 radius=0.20246189913346935 starId=-284738901 frame=true at=0,0,0 - body 4725059_6081849_-1115135 4725059_6081849_-1115135 kind=MOON orbit=0 radius=2.3125564194620427 starId=-28341585 frame=false at=44007,0,-10316 - body 4725059_6081849_-1115135 4725059_6081849_-1115135 kind=ROGUE_PLANET orbit=0 radius=0.4079580640952251 starId=-28341585 frame=true at=0,0,0 - body 5148034_-2724950_2998726 5147687_-2724953_2998875 kind=GAS_GIANT orbit=2019 radius=7.996125437815539 starId=-258191809 frame=true at=0,0,0 - body 5148034_-2724950_2998726 5147687_-2724953_2998875 kind=MOON orbit=2019 radius=0.35817784110343465 starId=-258191809 frame=false at=-1036390,0,1861088 - body 5148034_-2724950_2998726 5147687_-2724953_2998875 kind=MOON orbit=2019 radius=0.7157249758231874 starId=-258191809 frame=false at=-134387,0,-574491 - body 5148034_-2724950_2998726 5147811_-2724969_2998165 kind=ASTEROID_BELT orbit=3230 radius=0.0 starId=-258191809 frame=true at=0,0,0 - body 5148034_-2724950_2998726 5147837_-2724951_2998798 kind=ASTEROID_BELT orbit=1121 radius=0.0 starId=-258191809 frame=true at=0,0,0 - body 5148034_-2724950_2998726 5148028_-2724950_2998708 kind=PLANET orbit=102 radius=0.22373207447041826 starId=-258191809 frame=true at=0,0,0 - body 5148034_-2724950_2998726 5148030_-2724949_2998788 kind=PLANET orbit=334 radius=0.7535567326131476 starId=-258191809 frame=true at=0,0,0 - body 5148034_-2724950_2998726 5148034_-2724950_2998726 kind=STAR orbit=0 radius=0.0 starId=-258191809 frame=true at=0,0,0 - body 5549745_1612567_-2459150 5549745_1612567_-2459150 kind=ROGUE_PLANET orbit=0 radius=1.0153534844490044 starId=-910886457 frame=true at=0,0,0 - body 5569465_-2990317_6697828 5569089_-2990307_6697897 kind=PLANET orbit=2047 radius=2.113428500370335 starId=-1028453113 frame=true at=0,0,0 - body 5569465_-2990317_6697828 5569364_-2990322_6697902 kind=GAS_GIANT orbit=671 radius=4.609784144078134 starId=-1028453113 frame=true at=0,0,0 - body 5569465_-2990317_6697828 5569364_-2990322_6697902 kind=MOON orbit=671 radius=0.4880679780728293 starId=-1028453113 frame=false at=-775463,0,91138 - body 5569465_-2990317_6697828 5569364_-2990322_6697902 kind=MOON orbit=671 radius=0.5456917923931366 starId=-1028453113 frame=false at=-628683,0,-330366 - body 5569465_-2990317_6697828 5569437_-2990317_6697833 kind=GAS_GIANT orbit=151 radius=9.634001360033889 starId=-1028453113 frame=true at=0,0,0 - body 5569465_-2990317_6697828 5569462_-2990317_6697826 kind=STAR orbit=19 radius=78.81359558343887 starId=-1028453114 frame=true at=0,0,0 - body 5569465_-2990317_6697828 5569465_-2990317_6697828 kind=STAR orbit=0 radius=0.0 starId=-1028453113 frame=true at=0,0,0 - body 5569465_-2990317_6697828 5569476_-2990318_6697814 kind=PLANET orbit=95 radius=2.1269076094125596 starId=-1028453113 frame=true at=0,0,0 - body 5569465_-2990317_6697828 5569481_-2990317_6697828 kind=ASTEROID_BELT orbit=83 radius=0.0 starId=-1028453113 frame=true at=0,0,0 - body 5569465_-2990317_6697828 5569514_-2990315_6697863 kind=PLANET orbit=324 radius=0.35129067425550137 starId=-1028453113 frame=true at=0,0,0 - body 5569465_-2990317_6697828 5570051_-2990300_6698005 kind=ASTEROID_BELT orbit=3275 radius=0.0 starId=-1028453113 frame=true at=0,0,0 - body 6255448_968120_6199733 6255448_968120_6199733 kind=MOON orbit=0 radius=0.4200773190749709 starId=-886206973 frame=false at=25413,0,7149 - body 6255448_968120_6199733 6255448_968120_6199733 kind=ROGUE_PLANET orbit=0 radius=0.29383472367066443 starId=-886206973 frame=true at=0,0,0 - body 6513943_-990357_-607133 6513943_-990357_-607133 kind=MOON orbit=0 radius=1.0834140861728314 starId=-207608917 frame=false at=41645,0,21788 - body 6513943_-990357_-607133 6513943_-990357_-607133 kind=ROGUE_PLANET orbit=0 radius=0.5014717372599318 starId=-207608917 frame=true at=0,0,0 - body 6544140_1191032_686782 6544001_1191037_686867 kind=ASTEROID_BELT orbit=870 radius=0.0 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544042_1191031_686755 kind=MOON orbit=544 radius=0.31646082195635844 starId=-1188842121 frame=false at=-63556,0,-27372 - body 6544140_1191032_686782 6544042_1191031_686755 kind=PLANET orbit=544 radius=0.3603369419189195 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544063_1191032_686756 kind=GAS_GIANT orbit=436 radius=10.24404348301498 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544063_1191032_686756 kind=MOON orbit=436 radius=0.25995484068675223 starId=-1188842121 frame=false at=1020070,0,961217 - body 6544140_1191032_686782 6544063_1191032_686756 kind=MOON orbit=436 radius=0.6310614677749826 starId=-1188842121 frame=false at=775018,0,-2287263 - body 6544140_1191032_686782 6544127_1191032_686779 kind=MOON orbit=72 radius=0.2873039609709143 starId=-1188842121 frame=false at=-21656,0,5725 - body 6544140_1191032_686782 6544127_1191032_686779 kind=PLANET orbit=72 radius=0.21819393982079735 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544128_1191032_686758 kind=PLANET orbit=142 radius=0.34477288327841055 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544134_1191033_686798 kind=MOON orbit=92 radius=0.46934188484059625 starId=-1188842121 frame=false at=49734,0,5147 - body 6544140_1191032_686782 6544134_1191033_686798 kind=PLANET orbit=92 radius=0.2431769234018869 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544135_1191032_686776 kind=GAS_GIANT orbit=43 radius=5.489376420198329 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544135_1191032_686776 kind=MOON orbit=43 radius=0.6855114250089008 starId=-1188842121 frame=false at=-220475,0,795409 - body 6544140_1191032_686782 6544137_1191032_686776 kind=GAS_GIANT orbit=34 radius=5.445296272973473 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544139_1191031_686761 kind=PLANET orbit=110 radius=0.22450064981738688 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544139_1191032_686779 kind=MOON orbit=16 radius=0.7372001331992204 starId=-1188842121 frame=false at=362801,0,-348114 - body 6544140_1191032_686782 6544139_1191032_686779 kind=PLANET orbit=16 radius=1.8148705728399028 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544139_1191032_686783 kind=MOON orbit=8 radius=0.7273856792771569 starId=-1188842121 frame=false at=381033,0,-319720 - body 6544140_1191032_686782 6544139_1191032_686783 kind=PLANET orbit=8 radius=1.7790736666585254 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544140_1191032_686782 kind=STAR orbit=0 radius=0.0 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544141_1191032_686779 kind=ASTEROID_BELT orbit=18 radius=0.0 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544141_1191032_686782 kind=MOON orbit=7 radius=0.5669808783297221 starId=-1188842121 frame=false at=-124752,0,-517372 - body 6544140_1191032_686782 6544141_1191032_686782 kind=PLANET orbit=7 radius=2.0090225676405997 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544141_1191032_686784 kind=PLANET orbit=14 radius=1.7709663722662257 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544142_1191032_686781 kind=MOON orbit=11 radius=0.6023857494894139 starId=-1188842121 frame=false at=-328060,0,191772 - body 6544140_1191032_686782 6544142_1191032_686781 kind=PLANET orbit=11 radius=1.9402070673972158 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544142_1191032_686785 kind=PLANET orbit=21 radius=1.4212622127384715 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544145_1191032_686780 kind=PLANET orbit=28 radius=0.8401221729861281 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544146_1191032_686775 kind=PLANET orbit=52 radius=2.1414268787063544 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544158_1191032_686742 kind=PLANET orbit=232 radius=1.0270189716791964 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544172_1191032_686773 kind=MOON orbit=176 radius=0.7394920664400455 starId=-1188842121 frame=false at=148666,0,-34426 - body 6544140_1191032_686782 6544172_1191032_686773 kind=PLANET orbit=176 radius=0.8104176080813175 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544189_1191032_686805 kind=GAS_GIANT orbit=288 radius=10.446483309221662 starId=-1188842121 frame=true at=0,0,0 - body 6544140_1191032_686782 6544189_1191032_686805 kind=MOON orbit=288 radius=0.22806100230671833 starId=-1188842121 frame=false at=3049369,0,-37540 - body 6544140_1191032_686782 6544189_1191032_686805 kind=MOON orbit=288 radius=0.3903869875297332 starId=-1188842121 frame=false at=-2469126,0,-1818977 - body 6544140_1191032_686782 6544204_1191032_686802 kind=PLANET orbit=357 radius=0.20723184818615456 starId=-1188842121 frame=true at=0,0,0 - body 6799915_4871887_422782 6799897_4871887_422780 kind=GAS_GIANT orbit=95 radius=9.677622955960413 starId=-15767913 frame=true at=0,0,0 - body 6799915_4871887_422782 6799897_4871887_422780 kind=MOON orbit=95 radius=0.42484617416659975 starId=-15767913 frame=false at=1815244,0,-1531196 - body 6799915_4871887_422782 6799906_4871885_422865 kind=ASTEROID_BELT orbit=446 radius=0.0 starId=-15767913 frame=true at=0,0,0 - body 6799915_4871887_422782 6799913_4871887_422790 kind=GAS_GIANT orbit=43 radius=9.568051622775512 starId=-15767913 frame=true at=0,0,0 - body 6799915_4871887_422782 6799914_4871887_422781 kind=PLANET orbit=7 radius=1.103751530221875 starId=-15767913 frame=true at=0,0,0 - body 6799915_4871887_422782 6799915_4871887_422782 kind=STAR orbit=0 radius=0.0 starId=-15767913 frame=true at=0,0,0 - body 6799915_4871887_422782 6799916_4871887_422785 kind=MOON orbit=20 radius=0.25984113495172845 starId=-15767913 frame=false at=301214,0,-263496 - body 6799915_4871887_422782 6799916_4871887_422785 kind=PLANET orbit=20 radius=2.4101333906778977 starId=-15767913 frame=true at=0,0,0 - body 6799915_4871887_422782 6799919_4871887_422780 kind=ASTEROID_BELT orbit=23 radius=0.0 starId=-15767913 frame=true at=0,0,0 - body 6799915_4871887_422782 6799949_4871888_422822 kind=PLANET orbit=279 radius=0.2023591304452867 starId=-15767913 frame=true at=0,0,0 - body 772836_5636740_4927735 772836_5636740_4927735 kind=ROGUE_PLANET orbit=0 radius=0.8207995700352966 starId=-1020620017 frame=true at=0,0,0 - body 907102_1605031_1799741 907102_1605031_1799741 kind=MOON orbit=0 radius=1.6079605624709656 starId=-1806957165 frame=false at=-96755,0,540000 - body 907102_1605031_1799741 907102_1605031_1799741 kind=ROGUE_PLANET orbit=0 radius=2.2676109846054535 starId=-1806957165 frame=true at=0,0,0 - derived -1080086_1555356_4409113 -1080086_1555356_4409113 type=ice mass=16.50279570286089 radius=2.038268124956373 gravity=397 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=8240 metallicity=0.9541592391020977 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1834664_1670352_-1563056 -1834341_1670330_-1563572 type=ice mass=1.3589831485054766 radius=1.1181559977969782 gravity=109 pressure=1600 tempK=90 oxygen=false locked=false rings=false rotation=8770 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1834664_1670352_-1563056 -1834603_1670354_-1563099 type=gasgiant mass=69.44517257650985 radius=5.676685494197384 gravity=216 pressure=1600 tempK=271 oxygen=false locked=false rings=true rotation=5422 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1834664_1670352_-1563056 -1834656_1670351_-1563015 type=superearth mass=5.604436130311837 radius=1.6484907919486196 gravity=206 pressure=1600 tempK=397 oxygen=false locked=false rings=false rotation=55647 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1834664_1670352_-1563056 -1834664_1670352_-1563056 type=lava mass=6.297278147697221 radius=1.5654725816445216 gravity=257 pressure=19 tempK=2796 oxygen=false locked=true rings=false rotation=45683 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1834664_1670352_-1563056 -1834665_1670352_-1563062 type=barren mass=0.0036920729492058825 radius=0.217838595089208 gravity=8 pressure=0 tempK=476 oxygen=false locked=true rings=false rotation=17309 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1834664_1670352_-1563056 -1834674_1670352_-1563044 type=barren mass=0.0070392049784330805 radius=0.2706016219811198 gravity=10 pressure=0 tempK=303 oxygen=false locked=false rings=false rotation=58582 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1834664_1670352_-1563056 -1834697_1670352_-1563082 type=greenhouse mass=5.306353329769671 radius=1.5301172792246798 gravity=227 pressure=1600 tempK=304 oxygen=false locked=false rings=false rotation=12411 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1834664_1670352_-1563056 -1834844_1670355_-1562989 type=barren mass=0.04424796580738948 radius=0.4169178260700219 gravity=25 pressure=15 tempK=86 oxygen=false locked=false rings=false rotation=73360 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1834664_1670352_-1563056 -1835637_1670333_-1563103 type=icegiant mass=77.12815588696303 radius=5.941666373737659 gravity=218 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=13678 metallicity=0.43609260838850816 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2208092_-1790276_1685270 -2208092_-1790276_1685270 type=barren mass=0.005648418871458735 radius=0.2385909118845583 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=24775 metallicity=1.5155492707302036 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2290821_1403075_856935 -2290821_1403075_856935 type=ice mass=0.266707898716306 radius=0.6825852386298956 gravity=57 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=81338 metallicity=1.294876211758993 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3014847_-1420592_5288623 -3014847_-1420592_5288623 type=ice mass=0.005044358396498096 radius=0.2259652104199226 gravity=10 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=62265 metallicity=1.1198703399625591 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3374315_4976731_3972240 -3374315_4976731_3972240 type=ice mass=2.0498293005739696 radius=1.257168129941409 gravity=130 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=44853 metallicity=1.1089654140853202 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -436060_-2366191_-949510 -435942_-2366191_-949421 type=icegiant mass=246.21329912216967 radius=9.841958949883894 gravity=254 pressure=1600 tempK=125 oxygen=false locked=false rings=false rotation=4994 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -436060_-2366191_-949510 -436000_-2366193_-949507 type=ice mass=0.0025001436401144083 radius=0.20969431093726135 gravity=6 pressure=0 tempK=82 oxygen=false locked=false rings=false rotation=57151 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -436060_-2366191_-949510 -436025_-2366191_-949523 type=superearth mass=11.986869338529015 radius=1.9765339194370526 gravity=307 pressure=1600 tempK=268 oxygen=false locked=false rings=false rotation=40867 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -436060_-2366191_-949510 -436055_-2366191_-949514 type=desert mass=0.01731826282819392 radius=0.34512975441876653 gravity=15 pressure=0 tempK=289 oxygen=false locked=true rings=false rotation=51106 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -436060_-2366191_-949510 -436060_-2366191_-949510 type=lava mass=0.09081514066754195 radius=0.5369742020541469 gravity=31 pressure=0 tempK=1794 oxygen=false locked=true rings=false rotation=73337 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -436060_-2366191_-949510 -436062_-2366191_-949508 type=desert mass=1.1028863386362178 radius=0.98242013651816 gravity=114 pressure=64 tempK=470 oxygen=false locked=true rings=false rotation=58647 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -436060_-2366191_-949510 -436065_-2366191_-949500 type=greenhouse mass=2.5515226570009824 radius=1.334015205340586 gravity=143 pressure=991 tempK=330 oxygen=false locked=false rings=false rotation=63087 metallicity=0.47226004045928116 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -528291_6799917_1589264 -528281_6799917_1589255 type=greenhouse mass=4.411463451533969 radius=1.549060693570877 gravity=184 pressure=569 tempK=435 oxygen=false locked=false rings=false rotation=26130 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -528291_6799917_1589264 -528289_6799918_1589188 type=exotic mass=2.524783156469872 radius=1.2598310614277537 gravity=159 pressure=1600 tempK=302 oxygen=false locked=false rings=false rotation=24005 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -528291_6799917_1589264 -528291_6799917_1589264 type=lava mass=2.0192563355271664 radius=1.2060277237340613 gravity=139 pressure=2 tempK=2886 oxygen=false locked=true rings=false rotation=61672 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -528291_6799917_1589264 -528415_6799932_1589887 type=superearth mass=9.60122918654861 radius=1.9904254713501455 gravity=242 pressure=1600 tempK=104 oxygen=false locked=false rings=false rotation=18054 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -528291_6799917_1589264 -529048_6799943_1588586 type=gasgiant mass=102.21010191973816 radius=6.715430513074007 gravity=227 pressure=1600 tempK=76 oxygen=false locked=false rings=false rotation=5319 metallicity=1.2790626554252973 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -935767_4549081_-1731362 -935767_4549081_-1731362 type=ice mass=0.0028984057910889116 radius=0.21212769702860157 gravity=6 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=10990 metallicity=1.4772416207188779 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1683940_-183048_-1702546 1683940_-183048_-1702546 type=barren mass=0.010671389149843244 radius=0.28622983833858173 gravity=13 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=18839 metallicity=1.0655911026250537 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1920315_1641073_-2834001 1920315_1641073_-2834001 type=ice mass=0.10126275801176421 radius=0.5693690184380397 gravity=31 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=7760 metallicity=0.5352071006919491 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 233560_2922383_5010954 233560_2922383_5010954 type=superearth mass=4.212603209068318 radius=1.4350700127224734 gravity=205 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=20455 metallicity=1.2849716814294743 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 236691_5203788_1778447 236688_5203788_1778436 type=gasgiant mass=193.7262334816812 radius=8.86769297367945 gravity=246 pressure=1600 tempK=281 oxygen=false locked=false rings=true rotation=7749 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 236691_5203788_1778447 236690_5203788_1778446 type=lava mass=6.252037775157124 radius=1.5952759531425913 gravity=246 pressure=1600 tempK=802 oxygen=false locked=true rings=false rotation=87775 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 236691_5203788_1778447 236691_5203788_1778447 type=barren mass=0.05717937002910721 radius=0.4520758203563616 gravity=28 pressure=0 tempK=999 oxygen=false locked=true rings=false rotation=79433 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 236691_5203788_1778447 236693_5203788_1778447 type=barren mass=0.17809896703041364 radius=0.6434024890394139 gravity=43 pressure=2 tempK=291 oxygen=false locked=true rings=false rotation=28497 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 236691_5203788_1778447 236832_5203788_1778492 type=ice mass=10.574660573064731 radius=1.924690506299482 gravity=285 pressure=1600 tempK=80 oxygen=false locked=false rings=false rotation=35653 metallicity=0.6338296378768227 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2374829_-1121220_6754955 2369207_-1121220_6758455 type=icegiant mass=138.84327717357925 radius=7.672085602961861 gravity=236 pressure=1600 tempK=10 oxygen=false locked=false rings=true rotation=11389 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2374829_-1121220_6754955 2374772_-1121218_6754973 type=gasgiant mass=23.639860574930704 radius=3.553184930957361 gravity=187 pressure=1600 tempK=99 oxygen=false locked=false rings=false rotation=12132 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2374829_-1121220_6754955 2374790_-1121224_6755042 type=barren mass=0.004459332908709616 radius=0.23724534463610833 gravity=8 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=91424 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2374829_-1121220_6754955 2374827_-1121220_6754949 type=barren mass=0.13337484033974645 radius=0.5970847496460991 gravity=37 pressure=18 tempK=158 oxygen=false locked=true rings=false rotation=18630 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2374829_-1121220_6754955 2374829_-1121220_6754955 type=lava mass=14.647744112503442 radius=2.073549074884492 gravity=341 pressure=1075 tempK=1869 oxygen=false locked=true rings=false rotation=19737 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2374829_-1121220_6754955 2374830_-1121220_6754957 type=barren mass=0.002441342699551333 radius=0.2038047543227626 gravity=6 pressure=0 tempK=275 oxygen=false locked=true rings=false rotation=78813 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2374829_-1121220_6754955 2374835_-1121220_6754946 type=gasgiant mass=278.8962302254018 radius=10.39003058667359 gravity=258 pressure=1600 tempK=228 oxygen=false locked=false rings=false rotation=5158 metallicity=1.1938476285514823 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3353092_-2509636_325419 3353092_-2509636_325419 type=barren mass=0.06282138922649873 radius=0.5025877735087989 gravity=25 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=16027 metallicity=1.2404503262924214 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3392849_5972712_-904179 3392849_5972712_-904179 type=barren mass=0.009876212507077011 radius=0.27812854586282826 gravity=13 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=54299 metallicity=1.4252753140160968 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4690385_6611748_3834194 4690385_6611748_3834194 type=ice mass=0.0023842033479558543 radius=0.20246189913346935 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=9734 metallicity=0.41246374291164867 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4725059_6081849_-1115135 4725059_6081849_-1115135 type=ice mass=0.034968096229707576 radius=0.4079580640952251 gravity=21 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=45324 metallicity=0.9010689721776831 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5148034_-2724950_2998726 5147687_-2724953_2998875 type=gasgiant mass=152.70280703610828 radius=7.996125437815539 gravity=239 pressure=1600 tempK=132 oxygen=false locked=false rings=true rotation=7410 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5148034_-2724950_2998726 5147811_-2724969_2998165 type=barren mass=0.002992299640949069 radius=0.21770853518619304 gravity=6 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=20666 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5148034_-2724950_2998726 5147837_-2724951_2998798 type=ice mass=5.466740675672476 radius=1.6094577312496248 gravity=211 pressure=1600 tempK=167 oxygen=false locked=false rings=false rotation=6715 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5148034_-2724950_2998726 5148028_-2724950_2998708 type=barren mass=0.0041496893892041375 radius=0.22373207447041826 gravity=8 pressure=0 tempK=301 oxygen=false locked=false rings=false rotation=17131 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5148034_-2724950_2998726 5148030_-2724949_2998788 type=ice mass=0.36690758271858726 radius=0.7535567326131476 gravity=65 pressure=102 tempK=154 oxygen=false locked=false rings=false rotation=27675 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5148034_-2724950_2998726 5148034_-2724950_2998726 type=lava mass=3.417268629173799 radius=1.3915138241071623 gravity=176 pressure=3 tempK=3058 oxygen=false locked=true rings=false rotation=37045 metallicity=0.6363310433864842 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5549745_1612567_-2459150 5549745_1612567_-2459150 type=ice mass=1.050098511379873 radius=1.0153534844490044 gravity=102 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=24753 metallicity=0.3928649683191534 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5569465_-2990317_6697828 5569089_-2990307_6697897 type=ice mass=16.479662665760813 radius=2.113428500370335 gravity=369 pressure=1600 tempK=84 oxygen=false locked=false rings=false rotation=9024 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5569465_-2990317_6697828 5569364_-2990322_6697902 type=icegiant mass=43.02187167996671 radius=4.609784144078134 gravity=202 pressure=1600 tempK=156 oxygen=false locked=false rings=true rotation=12742 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5569465_-2990317_6697828 5569437_-2990317_6697833 type=gasgiant mass=234.4117416739366 radius=9.634001360033889 gravity=253 pressure=1600 tempK=330 oxygen=false locked=false rings=true rotation=9011 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5569465_-2990317_6697828 5569462_-2990317_6697826 type=barren mass=0.37214398915860636 radius=0.8179661755366368 gravity=56 pressure=7 tempK=474 oxygen=false locked=true rings=false rotation=80736 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5569465_-2990317_6697828 5569465_-2990317_6697828 type=lava mass=0.0021659933087211126 radius=0.2020085491799058 gravity=5 pressure=0 tempK=2067 oxygen=false locked=true rings=false rotation=23645 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5569465_-2990317_6697828 5569476_-2990318_6697814 type=superearth mass=18.11852475983095 radius=2.1269076094125596 gravity=400 pressure=1600 tempK=452 oxygen=false locked=false rings=false rotation=11585 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5569465_-2990317_6697828 5569481_-2990317_6697828 type=ocean mass=1.059189868092209 radius=0.9864908144267657 gravity=109 pressure=211 tempK=310 oxygen=false locked=false rings=false rotation=60621 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5569465_-2990317_6697828 5569514_-2990315_6697863 type=ice mass=0.02160095503404406 radius=0.35129067425550137 gravity=18 pressure=1 tempK=94 oxygen=false locked=false rings=false rotation=45792 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5569465_-2990317_6697828 5570051_-2990300_6698005 type=superearth mass=16.306973848511685 radius=2.212745593215032 gravity=333 pressure=1600 tempK=77 oxygen=false locked=false rings=false rotation=56640 metallicity=0.40866054299517585 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6255448_968120_6199733 6255448_968120_6199733 type=ice mass=0.013200272332713349 radius=0.29383472367066443 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=43984 metallicity=0.8253735417868293 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6513943_-990357_-607133 6513943_-990357_-607133 type=barren mass=0.09201947656728872 radius=0.5014717372599318 gravity=37 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=42447 metallicity=1.334877107093964 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544001_1191037_686867 type=ice mass=0.5984193937280294 radius=0.8686309348655668 gravity=79 pressure=1600 tempK=65 oxygen=false locked=false rings=false rotation=54198 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544042_1191031_686755 type=barren mass=0.018747194391930556 radius=0.3603369419189195 gravity=14 pressure=3 tempK=44 oxygen=false locked=false rings=false rotation=23367 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544063_1191032_686756 type=gasgiant mass=269.96545473267946 radius=10.24404348301498 gravity=257 pressure=1600 tempK=97 oxygen=false locked=false rings=true rotation=4821 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544127_1191032_686779 type=ice mass=0.0028044798010789514 radius=0.21819393982079735 gravity=6 pressure=0 tempK=101 oxygen=false locked=false rings=false rotation=6542 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544128_1191032_686758 type=barren mass=0.02158702650110108 radius=0.34477288327841055 gravity=18 pressure=3 tempK=87 oxygen=false locked=false rings=false rotation=20290 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544134_1191033_686798 type=barren mass=0.006171595858463438 radius=0.2431769234018869 gravity=10 pressure=0 tempK=109 oxygen=false locked=false rings=false rotation=85791 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544135_1191032_686776 type=gasgiant mass=64.28754965431646 radius=5.489376420198329 gravity=213 pressure=1600 tempK=311 oxygen=false locked=false rings=true rotation=8173 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544137_1191032_686776 type=gasgiant mass=63.106403051729444 radius=5.445296272973473 gravity=213 pressure=1600 tempK=350 oxygen=false locked=false rings=true rotation=10319 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544139_1191031_686761 type=barren mass=0.003125528676288277 radius=0.22450064981738688 gravity=6 pressure=0 tempK=99 oxygen=false locked=false rings=false rotation=9194 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544139_1191032_686779 type=superearth mass=9.957185823292782 radius=1.8148705728399028 gravity=302 pressure=1600 tempK=555 oxygen=false locked=true rings=false rotation=15904 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544139_1191032_686783 type=superearth mass=9.204815331500313 radius=1.7790736666585254 gravity=291 pressure=1600 tempK=786 oxygen=false locked=true rings=false rotation=16380 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544140_1191032_686782 type=lava mass=0.003495511330012961 radius=0.23077661816220355 gravity=7 pressure=0 tempK=1052 oxygen=false locked=true rings=false rotation=25695 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544141_1191032_686779 type=superearth mass=11.047638995008654 radius=1.9621716147310968 gravity=287 pressure=1600 tempK=524 oxygen=false locked=true rings=false rotation=14669 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544141_1191032_686782 type=superearth mass=11.967664795083397 radius=2.0090225676405997 gravity=297 pressure=1600 tempK=840 oxygen=false locked=true rings=false rotation=15220 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544141_1191032_686784 type=greenhouse mass=7.79141122491771 radius=1.7709663722662257 gravity=248 pressure=1600 tempK=459 oxygen=false locked=true rings=true rotation=10842 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544142_1191032_686781 type=lava mass=12.653119261304946 radius=1.9402070673972158 gravity=336 pressure=1600 tempK=713 oxygen=false locked=true rings=false rotation=11634 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544142_1191032_686785 type=superearth mass=4.019480587927113 radius=1.4212622127384715 gravity=199 pressure=1600 tempK=485 oxygen=false locked=true rings=false rotation=24322 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544145_1191032_686780 type=desert mass=0.546198035443908 radius=0.8401221729861281 gravity=77 pressure=156 tempK=234 oxygen=false locked=true rings=false rotation=31071 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544146_1191032_686775 type=superearth mass=20.051271375975926 radius=2.1414268787063544 gravity=400 pressure=1600 tempK=308 oxygen=false locked=false rings=false rotation=11752 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544158_1191032_686742 type=ice mass=1.0540998936610393 radius=1.0270189716791964 gravity=100 pressure=1600 tempK=126 oxygen=false locked=false rings=false rotation=13739 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544172_1191032_686773 type=ice mass=0.514057046161867 radius=0.8104176080813175 gravity=78 pressure=768 tempK=121 oxygen=false locked=false rings=true rotation=12337 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544189_1191032_686805 type=gasgiant mass=282.3938328204548 radius=10.446483309221662 gravity=259 pressure=1600 tempK=120 oxygen=false locked=false rings=false rotation=12786 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6544140_1191032_686782 6544204_1191032_686802 type=ice mass=0.003386499072493738 radius=0.20723184818615456 gravity=8 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=36624 metallicity=1.257394134338192 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6799915_4871887_422782 6799897_4871887_422780 type=icegiant mass=236.86012225174596 radius=9.677622955960413 gravity=253 pressure=1600 tempK=179 oxygen=false locked=false rings=true rotation=7323 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6799915_4871887_422782 6799906_4871885_422865 type=ice mass=0.21716084645281308 radius=0.6802498305881435 gravity=47 pressure=180 tempK=45 oxygen=false locked=false rings=false rotation=69571 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6799915_4871887_422782 6799913_4871887_422790 type=gasgiant mass=230.7374117373395 radius=9.568051622775512 gravity=252 pressure=1600 tempK=266 oxygen=false locked=false rings=true rotation=5781 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6799915_4871887_422782 6799914_4871887_422781 type=greenhouse mass=1.371342956962792 radius=1.103751530221875 gravity=113 pressure=245 tempK=347 oxygen=false locked=true rings=false rotation=10002 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6799915_4871887_422782 6799915_4871887_422782 type=lava mass=10.748140594691607 radius=2.042247167653855 gravity=258 pressure=160 tempK=1139 oxygen=false locked=true rings=false rotation=30465 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6799915_4871887_422782 6799916_4871887_422785 type=greenhouse mass=22.039018182220467 radius=2.4101333906778977 gravity=379 pressure=1600 tempK=328 oxygen=false locked=true rings=false rotation=13224 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6799915_4871887_422782 6799919_4871887_422780 type=ice mass=0.02972947091143899 radius=0.40514559833386565 gravity=18 pressure=1 tempK=153 oxygen=false locked=true rings=false rotation=65714 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6799915_4871887_422782 6799949_4871888_422822 type=barren mass=0.0028085340043301683 radius=0.2023591304452867 gravity=7 pressure=0 tempK=53 oxygen=false locked=false rings=false rotation=9454 metallicity=1.5181303211087824 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 772836_5636740_4927735 772836_5636740_4927735 type=ice mass=0.5889054698003752 radius=0.8207995700352966 gravity=87 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=56192 metallicity=0.5085707964711265 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 907102_1605031_1799741 907102_1605031_1799741 type=ice mass=20.825124090311277 radius=2.2676109846054535 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=36240 metallicity=0.47844268929701317 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1080086_1555356_4409113 id=-1337008461 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless - system -1834664_1670352_-1563056 id=-1729416869 kind=STAR name=PGS--3525313.0.-3525313 starTemp=100 starSize=1.0540038347244263 - system -2208092_-1790276_1685270 id=-1765959065 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless - system -2290821_1403075_856935 id=-1992245269 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless - system -3014847_-1420592_5288623 id=-794196001 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless - system -3374315_4976731_3972240 id=-1160817649 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless - system -436060_-2366191_-949510 id=-498839401 kind=STAR name=PGS--3525313.-3525313.-3525313 starTemp=70 starSize=0.8857885003089905 - system -528291_6799917_1589264 id=-1828554265 kind=STAR name=PGS--3525313.3525313.0 starTemp=100 starSize=1.122846007347107 - system -935767_4549081_-1731362 id=-1358211061 kind=ROGUE_PLANET name=PGR--3525313.3525313.-3525313 starless - system 1683940_-183048_-1702546 id=-167956389 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless - system 1920315_1641073_-2834001 id=-1131707005 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless - system 233560_2922383_5010954 id=-1390378301 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless - system 236691_5203788_1778447 id=-1521889893 kind=STAR name=PGS-0.3525313.0 starTemp=40 starSize=0.8503480553627014 - system 2374829_-1121220_6754955 id=-1897301649 kind=STAR name=PGS-0.-3525313.3525313 starTemp=40 starSize=0.709514319896698 - system 3353092_-2509636_325419 id=-1078102009 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless - system 3392849_5972712_-904179 id=-1470468277 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless - system 4690385_6611748_3834194 id=-284738901 kind=ROGUE_PLANET name=PGR-3525313.3525313.3525313 starless - system 4725059_6081849_-1115135 id=-28341585 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless - system 5148034_-2724950_2998726 id=-258191809 kind=STAR name=PGS-3525313.-3525313.0 starTemp=100 starSize=1.2604737281799316 - system 5549745_1612567_-2459150 id=-910886457 kind=ROGUE_PLANET name=PGR-3525313.0.-3525313 starless - system 5569465_-2990317_6697828 id=-1028453113 kind=STAR name=PGS-3525313.-3525313.3525313 starTemp=70 starSize=1.1755964756011963 - system 6255448_968120_6199733 id=-886206973 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless - system 6513943_-990357_-607133 id=-207608917 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless - system 6544140_1191032_686782 id=-1188842121 kind=STAR name=PGS-3525313.0.0 starTemp=40 starSize=0.9326234459877014 - system 6799915_4871887_422782 id=-15767913 kind=STAR name=PGS-3525313.3525313.0 starTemp=40 starSize=0.6828064918518066 - system 772836_5636740_4927735 id=-1020620017 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless - system 907102_1605031_1799741 id=-1806957165 kind=ROGUE_PLANET name=PGR-0.0.0 starless -seed 6942069 systems=27 - body -1295452_590737_6017888 -1295070_590746_6017369 kind=MOON orbit=3445 radius=0.3190080862601766 starId=-871681409 frame=false at=-138529,0,-254546 - body -1295452_590737_6017888 -1295070_590746_6017369 kind=MOON orbit=3445 radius=0.7116873727132678 starId=-871681409 frame=false at=120234,0,261922 - body -1295452_590737_6017888 -1295070_590746_6017369 kind=PLANET orbit=3445 radius=1.2070218778244706 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295073_590747_6019489 kind=GAS_GIANT orbit=8799 radius=6.682294021731574 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295073_590747_6019489 kind=MOON orbit=8799 radius=0.20311680067120372 starId=-871681409 frame=false at=-1297409,0,451118 - body -1295452_590737_6017888 -1295073_590747_6019489 kind=MOON orbit=8799 radius=0.41603813684826363 starId=-871681409 frame=false at=-1143764,0,-1222873 - body -1295452_590737_6017888 -1295073_590747_6019489 kind=MOON orbit=8799 radius=0.6870083437381647 starId=-871681409 frame=false at=-296041,0,-393218 - body -1295452_590737_6017888 -1295358_590738_6017838 kind=ASTEROID_BELT orbit=571 radius=0.0 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295387_590731_6018000 kind=MOON orbit=693 radius=0.3784651847896745 starId=-871681409 frame=false at=254807,0,116075 - body -1295452_590737_6017888 -1295387_590731_6018000 kind=MOON orbit=693 radius=0.6783165555531443 starId=-871681409 frame=false at=-509904,0,204543 - body -1295452_590737_6017888 -1295387_590731_6018000 kind=PLANET orbit=693 radius=2.2787722785946882 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295442_590736_6017862 kind=PLANET orbit=149 radius=1.5837503051367467 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295451_590737_6017973 kind=PLANET orbit=452 radius=1.123888274347984 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295452_590737_6017888 kind=STAR orbit=0 radius=0.0 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295454_590737_6017903 kind=PLANET orbit=83 radius=0.20116599292739867 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295493_590722_6018208 kind=GAS_GIANT orbit=1727 radius=7.507047019723977 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295493_590722_6018208 kind=MOON orbit=1727 radius=0.31306945689191784 starId=-871681409 frame=false at=-1019710,0,874287 - body -1295452_590737_6017888 -1295496_590735_6017878 kind=PLANET orbit=242 radius=0.7098331686844603 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295585_590736_6017749 kind=GAS_GIANT orbit=1028 radius=10.409352129035774 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295585_590736_6017749 kind=MOON orbit=1028 radius=0.25777039740738394 starId=-871681409 frame=false at=107673,0,799986 - body -1295452_590737_6017888 -1295812_590729_6018754 kind=MOON orbit=5017 radius=0.27760010462297624 starId=-871681409 frame=false at=-269255,0,-120037 - body -1295452_590737_6017888 -1295812_590729_6018754 kind=MOON orbit=5017 radius=0.5125251114922812 starId=-871681409 frame=false at=138754,0,220117 - body -1295452_590737_6017888 -1295812_590729_6018754 kind=PLANET orbit=5017 radius=1.0093703974830894 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1295861_590671_6015628 kind=PLANET orbit=12287 radius=0.9155336635218376 starId=-871681409 frame=true at=0,0,0 - body -1295452_590737_6017888 -1299084_590613_6018440 kind=ASTEROID_BELT orbit=19659 radius=0.0 starId=-871681409 frame=true at=0,0,0 - body -1420862_99037_1766933 -1420862_99037_1766933 kind=MOON orbit=0 radius=0.49299935448250815 starId=-280597797 frame=false at=57791,0,9701 - body -1420862_99037_1766933 -1420862_99037_1766933 kind=MOON orbit=0 radius=1.4713603847399748 starId=-280597797 frame=false at=45046,0,138246 - body -1420862_99037_1766933 -1420862_99037_1766933 kind=ROGUE_PLANET orbit=0 radius=0.5046332779050597 starId=-280597797 frame=true at=0,0,0 - body -1488035_-3034313_-3156807 -1488035_-3034313_-3156807 kind=ROGUE_PLANET orbit=0 radius=1.0104407160721844 starId=-401453429 frame=true at=0,0,0 - body -2011949_3789060_5390006 -2011949_3789060_5390006 kind=ROGUE_PLANET orbit=0 radius=1.8294486117039206 starId=-66168209 frame=true at=0,0,0 - body -268539_1650411_-2625440 -268539_1650411_-2625440 kind=ROGUE_PLANET orbit=0 radius=0.2601853496316905 starId=-1871662501 frame=true at=0,0,0 - body -3063801_-759422_5862386 -3063743_-759422_5862350 kind=ASTEROID_BELT orbit=366 radius=0.0 starId=-1332799893 frame=true at=0,0,0 - body -3063801_-759422_5862386 -3063792_-759422_5862390 kind=MOON orbit=53 radius=0.574243829678742 starId=-1332799893 frame=false at=-2228,0,-34929 - body -3063801_-759422_5862386 -3063792_-759422_5862390 kind=MOON orbit=53 radius=0.6652106597419126 starId=-1332799893 frame=false at=7281,0,96125 - body -3063801_-759422_5862386 -3063792_-759422_5862390 kind=PLANET orbit=53 radius=0.38115810884355894 starId=-1332799893 frame=true at=0,0,0 - body -3063801_-759422_5862386 -3063799_-759422_5862382 kind=PLANET orbit=22 radius=1.6780172812581946 starId=-1332799893 frame=true at=0,0,0 - body -3063801_-759422_5862386 -3063801_-759422_5862386 kind=STAR orbit=0 radius=0.0 starId=-1332799893 frame=true at=0,0,0 - body -3063801_-759422_5862386 -3063803_-759422_5862385 kind=MOON orbit=11 radius=0.2000098685870336 starId=-1332799893 frame=false at=-83170,0,16546 - body -3063801_-759422_5862386 -3063803_-759422_5862385 kind=PLANET orbit=11 radius=0.5061482545056208 starId=-1332799893 frame=true at=0,0,0 - body -3063801_-759422_5862386 -3063812_-759422_5862406 kind=PLANET orbit=121 radius=0.3235908932389939 starId=-1332799893 frame=true at=0,0,0 - body -3063801_-759422_5862386 -3063836_-759422_5862361 kind=PLANET orbit=229 radius=1.7102798695984978 starId=-1332799893 frame=true at=0,0,0 - body -3063801_-759422_5862386 -3063922_-759422_5862448 kind=STAR orbit=728 radius=88.8110494530201 starId=-1332799894 frame=true at=0,0,0 - body -3359999_5029417_-1201971 -3359999_5029417_-1201971 kind=ROGUE_PLANET orbit=0 radius=0.2756640180131294 starId=-63962517 frame=true at=0,0,0 - body -464282_-3293100_220531 -464282_-3293100_220531 kind=ROGUE_PLANET orbit=0 radius=1.7196633145190408 starId=-1130433613 frame=true at=0,0,0 - body -589874_5099752_2648961 -589845_5099756_2648834 kind=ASTEROID_BELT orbit=699 radius=0.0 starId=-573199273 frame=true at=0,0,0 - body -589874_5099752_2648961 -589846_5099755_2649038 kind=PLANET orbit=437 radius=1.3085576757295254 starId=-573199273 frame=true at=0,0,0 - body -589874_5099752_2648961 -589856_5099752_2648936 kind=GAS_GIANT orbit=164 radius=7.982010427523395 starId=-573199273 frame=true at=0,0,0 - body -589874_5099752_2648961 -589856_5099752_2648936 kind=MOON orbit=164 radius=0.593769818824629 starId=-573199273 frame=false at=639497,0,-548651 - body -589874_5099752_2648961 -589872_5099751_2648944 kind=ASTEROID_BELT orbit=91 radius=0.0 starId=-573199273 frame=true at=0,0,0 - body -589874_5099752_2648961 -589874_5099752_2648960 kind=PLANET orbit=8 radius=0.4372934321766312 starId=-573199273 frame=true at=0,0,0 - body -589874_5099752_2648961 -589874_5099752_2648961 kind=STAR orbit=0 radius=0.0 starId=-573199273 frame=true at=0,0,0 - body -589874_5099752_2648961 -589875_5099752_2648965 kind=PLANET orbit=23 radius=0.9102675836184433 starId=-573199273 frame=true at=0,0,0 - body -589874_5099752_2648961 -589876_5099752_2648973 kind=PLANET orbit=66 radius=0.5502112106561361 starId=-573199273 frame=true at=0,0,0 - body -589874_5099752_2648961 -590614_5099752_2650343 kind=STAR orbit=8383 radius=88.68083709418774 starId=-573199274 frame=true at=0,0,0 - body 115515_4884922_-1449848 115515_4884922_-1449848 kind=ROGUE_PLANET orbit=0 radius=2.182709991409553 starId=-1386063681 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1339940_-2645564_6560548 kind=ASTEROID_BELT orbit=625 radius=0.0 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340044_-2645561_6560536 kind=GAS_GIANT orbit=135 radius=4.438597591383325 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340052_-2645562_6560553 kind=MOON orbit=34 radius=0.37367358790291005 starId=-1921583641 frame=false at=66587,0,-1326 - body 1340056_-2645562_6560558 1340052_-2645562_6560553 kind=PLANET orbit=34 radius=0.27373337680420534 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340053_-2645562_6560556 kind=PLANET orbit=18 radius=1.6681649801111824 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340056_-2645562_6560558 kind=STAR orbit=0 radius=0.0 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340057_-2645562_6560559 kind=PLANET orbit=7 radius=0.22651086590250133 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340058_-2645562_6560557 kind=MOON orbit=11 radius=0.2027678761726554 starId=-1921583641 frame=false at=-22054,0,18521 - body 1340056_-2645562_6560558 1340058_-2645562_6560557 kind=MOON orbit=11 radius=0.736864067127678 starId=-1921583641 frame=false at=16307,0,73001 - body 1340056_-2645562_6560558 1340058_-2645562_6560557 kind=PLANET orbit=11 radius=0.2522099109479354 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340061_-2645562_6560554 kind=ASTEROID_BELT orbit=34 radius=0.0 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=GAS_GIANT orbit=62 radius=6.729055091962263 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=MOON orbit=62 radius=0.21906139343685416 starId=-1921583641 frame=false at=812207,0,555496 - body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=MOON orbit=62 radius=0.31490040064715435 starId=-1921583641 frame=false at=361247,0,754142 - body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=MOON orbit=62 radius=0.44318684402677405 starId=-1921583641 frame=false at=-843448,0,509904 - body 1340056_-2645562_6560558 1340064_-2645562_6560567 kind=MOON orbit=62 radius=0.5348797147409434 starId=-1921583641 frame=false at=-946432,0,941537 - body 1340056_-2645562_6560558 1340073_-2645562_6560550 kind=PLANET orbit=102 radius=1.1216746451827144 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340102_-2645562_6560536 kind=MOON orbit=273 radius=0.21610846864650235 starId=-1921583641 frame=false at=23894,0,-2257 - body 1340056_-2645562_6560558 1340102_-2645562_6560536 kind=PLANET orbit=273 radius=0.26426323362501386 starId=-1921583641 frame=true at=0,0,0 - body 1340056_-2645562_6560558 1340119_-2645561_6560521 kind=MOON orbit=391 radius=0.2095120881778693 starId=-1921583641 frame=false at=-201046,0,-312517 - body 1340056_-2645562_6560558 1340119_-2645561_6560521 kind=MOON orbit=391 radius=0.5144584346323176 starId=-1921583641 frame=false at=-15563,0,221053 - body 1340056_-2645562_6560558 1340119_-2645561_6560521 kind=PLANET orbit=391 radius=2.198756624518602 starId=-1921583641 frame=true at=0,0,0 - body 136345_2380618_4435608 136345_2380618_4435608 kind=MOON orbit=0 radius=0.32784282519564134 starId=-655881041 frame=false at=158566,0,-34143 - body 136345_2380618_4435608 136345_2380618_4435608 kind=MOON orbit=0 radius=0.4451578677661996 starId=-655881041 frame=false at=-160991,0,1706 - body 136345_2380618_4435608 136345_2380618_4435608 kind=ROGUE_PLANET orbit=0 radius=0.5726161013605666 starId=-655881041 frame=true at=0,0,0 - body 2294785_6087239_1852785 2294785_6087239_1852785 kind=ROGUE_PLANET orbit=0 radius=0.3788569313823317 starId=-747899749 frame=true at=0,0,0 - body 2324700_3395074_-773152 2324700_3395074_-773152 kind=MOON orbit=0 radius=1.5614013723674798 starId=-333451093 frame=false at=-38138,0,-81078 - body 2324700_3395074_-773152 2324700_3395074_-773152 kind=ROGUE_PLANET orbit=0 radius=0.32820893486546776 starId=-333451093 frame=true at=0,0,0 - body 2380220_-2985326_2261989 2380220_-2985326_2261989 kind=MOON orbit=0 radius=2.2315871511185916 starId=-846895665 frame=false at=-107775,0,-93683 - body 2380220_-2985326_2261989 2380220_-2985326_2261989 kind=ROGUE_PLANET orbit=0 radius=0.8176934272940033 starId=-846895665 frame=true at=0,0,0 - body 2455719_-1575919_-1684641 2455719_-1575919_-1684641 kind=MOON orbit=0 radius=0.24593919185767682 starId=-1093289653 frame=false at=19349,0,-21869 - body 2455719_-1575919_-1684641 2455719_-1575919_-1684641 kind=MOON orbit=0 radius=1.4677044102869057 starId=-1093289653 frame=false at=35895,0,38722 - body 2455719_-1575919_-1684641 2455719_-1575919_-1684641 kind=ROGUE_PLANET orbit=0 radius=0.23495795814274667 starId=-1093289653 frame=true at=0,0,0 - body 4041882_708425_6811475 4041882_708425_6811475 kind=MOON orbit=0 radius=0.7690545770847357 starId=-1527098829 frame=false at=159065,0,-248917 - body 4041882_708425_6811475 4041882_708425_6811475 kind=ROGUE_PLANET orbit=0 radius=1.029137364803036 starId=-1527098829 frame=true at=0,0,0 - body 4374645_5543557_6609018 4374645_5543557_6609018 kind=ROGUE_PLANET orbit=0 radius=1.0349140608384135 starId=-138926061 frame=true at=0,0,0 - body 4760881_3403866_-1378830 4760881_3403866_-1378830 kind=MOON orbit=0 radius=0.947747417710435 starId=-1839995337 frame=false at=273547,0,-11762 - body 4760881_3403866_-1378830 4760881_3403866_-1378830 kind=ROGUE_PLANET orbit=0 radius=1.244696857260704 starId=-1839995337 frame=true at=0,0,0 - body 5532167_-2797664_-2258523 5532167_-2797664_-2258523 kind=MOON orbit=0 radius=1.1673146084339747 starId=-850423465 frame=false at=10209,0,24779 - body 5532167_-2797664_-2258523 5532167_-2797664_-2258523 kind=ROGUE_PLANET orbit=0 radius=0.41943619401321175 starId=-850423465 frame=true at=0,0,0 - body 5628766_5108790_338559 5628766_5108790_338559 kind=MOON orbit=0 radius=0.3769649461819978 starId=-1665662161 frame=false at=-34548,0,-49544 - body 5628766_5108790_338559 5628766_5108790_338559 kind=MOON orbit=0 radius=1.7503827766419675 starId=-1665662161 frame=false at=-76783,0,-17714 - body 5628766_5108790_338559 5628766_5108790_338559 kind=ROGUE_PLANET orbit=0 radius=0.28277463573335193 starId=-1665662161 frame=true at=0,0,0 - body 5670369_-3422417_6764355 5670369_-3422417_6764355 kind=ROGUE_PLANET orbit=0 radius=1.0232019832097992 starId=-862267757 frame=true at=0,0,0 - body 5876998_3296879_3027393 5876998_3296879_3027393 kind=ROGUE_PLANET orbit=0 radius=2.0861890597953177 starId=-286545925 frame=true at=0,0,0 - body 6068647_-3169217_818787 6068647_-3169217_818787 kind=MOON orbit=0 radius=0.6312789796589846 starId=-621289557 frame=false at=-3640,0,121746 - body 6068647_-3169217_818787 6068647_-3169217_818787 kind=MOON orbit=0 radius=0.7024140531407117 starId=-621289557 frame=false at=45735,0,-64415 - body 6068647_-3169217_818787 6068647_-3169217_818787 kind=ROGUE_PLANET orbit=0 radius=0.4506215156332336 starId=-621289557 frame=true at=0,0,0 - body 668050_2680028_1601017 668050_2680028_1601017 kind=ROGUE_PLANET orbit=0 radius=1.3571907745157803 starId=-1525225641 frame=true at=0,0,0 - body 6948671_5049241_-2964475 6948671_5049241_-2964475 kind=MOON orbit=0 radius=0.7110288498579667 starId=-162398185 frame=false at=-41898,0,82130 - body 6948671_5049241_-2964475 6948671_5049241_-2964475 kind=ROGUE_PLANET orbit=0 radius=0.3116116058746792 starId=-162398185 frame=true at=0,0,0 - body 728798_4100023_3876685 728752_4100025_3876660 kind=GAS_GIANT orbit=278 radius=9.366458180023432 starId=-1903899713 frame=true at=0,0,0 - body 728798_4100023_3876685 728752_4100025_3876660 kind=MOON orbit=278 radius=0.2017256452460188 starId=-1903899713 frame=false at=-497064,0,-1468560 - body 728798_4100023_3876685 728752_4100025_3876660 kind=MOON orbit=278 radius=0.30964028113932174 starId=-1903899713 frame=false at=-1615396,0,-1813716 - body 728798_4100023_3876685 728752_4100025_3876660 kind=MOON orbit=278 radius=0.3987516690238629 starId=-1903899713 frame=false at=-1087247,0,-1632238 - body 728798_4100023_3876685 728752_4100025_3876660 kind=MOON orbit=278 radius=0.5538235729148501 starId=-1903899713 frame=false at=-1196641,0,1314230 - body 728798_4100023_3876685 728795_4100023_3876676 kind=GAS_GIANT orbit=52 radius=7.051145770054424 starId=-1903899713 frame=true at=0,0,0 - body 728798_4100023_3876685 728795_4100023_3876676 kind=MOON orbit=52 radius=0.21599279470357996 starId=-1903899713 frame=false at=-511636,0,-207734 - body 728798_4100023_3876685 728795_4100023_3876676 kind=MOON orbit=52 radius=0.3824736496878405 starId=-1903899713 frame=false at=-1026229,0,-1490958 - body 728798_4100023_3876685 728795_4100023_3876676 kind=MOON orbit=52 radius=0.5086753545023199 starId=-1903899713 frame=false at=473884,0,-213115 - body 728798_4100023_3876685 728798_4100023_3876682 kind=MOON orbit=14 radius=0.24619612572853286 starId=-1903899713 frame=false at=-272907,0,-251322 - body 728798_4100023_3876685 728798_4100023_3876682 kind=MOON orbit=14 radius=0.37447857405867035 starId=-1903899713 frame=false at=-104608,0,274760 - body 728798_4100023_3876685 728798_4100023_3876682 kind=PLANET orbit=14 radius=1.81070986321312 starId=-1903899713 frame=true at=0,0,0 - body 728798_4100023_3876685 728798_4100023_3876685 kind=STAR orbit=0 radius=0.0 starId=-1903899713 frame=true at=0,0,0 - body 728798_4100023_3876685 728802_4100023_3876688 kind=ASTEROID_BELT orbit=28 radius=0.0 starId=-1903899713 frame=true at=0,0,0 - body 728798_4100023_3876685 728826_4100019_3876763 kind=ASTEROID_BELT orbit=444 radius=0.0 starId=-1903899713 frame=true at=0,0,0 - derived -1295452_590737_6017888 -1295070_590746_6017369 type=superearth mass=2.3671386651819746 radius=1.2070218778244706 gravity=162 pressure=1600 tempK=189 oxygen=false locked=false rings=false rotation=12841 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295073_590747_6019489 type=gasgiant mass=101.05383037726808 radius=6.682294021731574 gravity=226 pressure=1600 tempK=108 oxygen=false locked=false rings=true rotation=5771 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295358_590738_6017838 type=superearth mass=8.602401240662678 radius=1.7689241251777645 gravity=275 pressure=1600 tempK=465 oxygen=false locked=false rings=false rotation=15646 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295387_590731_6018000 type=greenhouse mass=19.216360137189277 radius=2.2787722785946882 gravity=370 pressure=1600 tempK=326 oxygen=false locked=false rings=false rotation=79585 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295442_590736_6017862 type=lava mass=5.4132542997814035 radius=1.5837503051367467 gravity=216 pressure=1600 tempK=969 oxygen=false locked=false rings=false rotation=25383 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295451_590737_6017973 type=desert mass=1.258015523087503 radius=1.123888274347984 gravity=100 pressure=181 tempK=303 oxygen=false locked=false rings=false rotation=40088 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295452_590737_6017888 type=lava mass=0.01666233092074892 radius=0.33359015825984484 gravity=15 pressure=0 tempK=5260 oxygen=false locked=true rings=true rotation=31042 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295454_590737_6017903 type=barren mass=0.002928379387151399 radius=0.20116599292739867 gravity=7 pressure=0 tempK=574 oxygen=false locked=false rings=false rotation=30799 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295493_590722_6018208 type=gasgiant mass=132.06962874363816 radius=7.507047019723977 gravity=234 pressure=1600 tempK=245 oxygen=false locked=false rings=true rotation=7629 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295496_590735_6017878 type=desert mass=0.3300211540029742 radius=0.7098331686844603 gravity=65 pressure=3 tempK=317 oxygen=false locked=false rings=false rotation=22990 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295585_590736_6017749 type=gasgiant mass=280.0905487859904 radius=10.409352129035774 gravity=258 pressure=1600 tempK=318 oxygen=false locked=false rings=false rotation=4895 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295812_590729_6018754 type=ice mass=0.8308521390281213 radius=1.0093703974830894 gravity=82 pressure=1600 tempK=136 oxygen=false locked=false rings=false rotation=6508 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1295861_590671_6015628 type=ice mass=0.7054345416786596 radius=0.9155336635218376 gravity=84 pressure=1068 tempK=78 oxygen=false locked=false rings=false rotation=40584 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1295452_590737_6017888 -1299084_590613_6018440 type=ice mass=1.6788893782433592 radius=1.1927029201011403 gravity=118 pressure=1600 tempK=68 oxygen=false locked=false rings=false rotation=20534 metallicity=0.6150012443476253 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1420862_99037_1766933 -1420862_99037_1766933 type=ice mass=0.06081940610147002 radius=0.5046332779050597 gravity=24 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=37662 metallicity=1.292165131499846 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1488035_-3034313_-3156807 -1488035_-3034313_-3156807 type=ice mass=0.9712229971716803 radius=1.0104407160721844 gravity=95 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=9188 metallicity=1.0271627977509437 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2011949_3789060_5390006 -2011949_3789060_5390006 type=superearth mass=7.13242712054119 radius=1.8294486117039206 gravity=213 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=24601 metallicity=0.8716874569679396 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -268539_1650411_-2625440 -268539_1650411_-2625440 type=barren mass=0.008533012927036692 radius=0.2601853496316905 gravity=13 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=77028 metallicity=0.5151362933473932 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3063801_-759422_5862386 -3063743_-759422_5862350 type=ice mass=0.6171225552081331 radius=0.8339228503278076 gravity=89 pressure=1600 tempK=189 oxygen=false locked=false rings=false rotation=48269 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3063801_-759422_5862386 -3063792_-759422_5862390 type=desert mass=0.0214744582127459 radius=0.38115810884355894 gravity=15 pressure=0 tempK=246 oxygen=false locked=false rings=false rotation=45982 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3063801_-759422_5862386 -3063799_-759422_5862382 type=lava mass=7.208152273784709 radius=1.6780172812581946 gravity=256 pressure=1600 tempK=915 oxygen=false locked=true rings=false rotation=41175 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3063801_-759422_5862386 -3063801_-759422_5862386 type=lava mass=0.025606191045051286 radius=0.36006011176857755 gravity=20 pressure=0 tempK=1909 oxygen=false locked=true rings=false rotation=76354 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3063801_-759422_5862386 -3063803_-759422_5862385 type=barren mass=0.08511374116631085 radius=0.5061482545056208 gravity=33 pressure=0 tempK=572 oxygen=false locked=true rings=false rotation=35206 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3063801_-759422_5862386 -3063812_-759422_5862406 type=barren mass=0.011811053688285887 radius=0.3235908932389939 gravity=11 pressure=0 tempK=173 oxygen=false locked=false rings=false rotation=7776 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3063801_-759422_5862386 -3063836_-759422_5862361 type=superearth mass=6.341038709712936 radius=1.7102798695984978 gravity=217 pressure=1600 tempK=270 oxygen=false locked=false rings=false rotation=28895 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3063801_-759422_5862386 -3063922_-759422_5862448 type=ice mass=26.458866731696542 radius=2.4658749028334705 gravity=400 pressure=1600 tempK=139 oxygen=false locked=false rings=false rotation=15195 metallicity=0.6092532274168906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3359999_5029417_-1201971 -3359999_5029417_-1201971 type=ice mass=0.007999771937505304 radius=0.2756640180131294 gravity=11 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=55540 metallicity=1.512849261669333 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -464282_-3293100_220531 -464282_-3293100_220531 type=ice mass=5.685474320006944 radius=1.7196633145190408 gravity=192 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=76473 metallicity=0.3614643069971393 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -589874_5099752_2648961 -589845_5099756_2648834 type=ice mass=0.5680391705600453 radius=0.8307933662447684 gravity=82 pressure=1600 tempK=68 oxygen=false locked=false rings=false rotation=9715 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -589874_5099752_2648961 -589846_5099755_2649038 type=ice mass=3.326686517747295 radius=1.3085576757295254 gravity=194 pressure=1600 tempK=86 oxygen=false locked=false rings=false rotation=34331 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -589874_5099752_2648961 -589856_5099752_2648936 type=gasgiant mass=152.08354001896822 radius=7.982010427523395 gravity=239 pressure=1600 tempK=148 oxygen=false locked=false rings=false rotation=5558 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -589874_5099752_2648961 -589872_5099751_2648944 type=exotic mass=4.823300939179849 radius=1.5984511753710242 gravity=189 pressure=1600 tempK=217 oxygen=false locked=false rings=false rotation=35891 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -589874_5099752_2648961 -589874_5099752_2648960 type=desert mass=0.04458684623554106 radius=0.4372934321766312 gravity=23 pressure=0 tempK=326 oxygen=false locked=true rings=false rotation=11111 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -589874_5099752_2648961 -589874_5099752_2648961 type=lava mass=0.04313182269041765 radius=0.43322616684121373 gravity=23 pressure=0 tempK=982 oxygen=false locked=true rings=false rotation=11193 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -589874_5099752_2648961 -589875_5099752_2648965 type=desert mass=0.7355676541554571 radius=0.9102675836184433 gravity=89 pressure=100 tempK=216 oxygen=false locked=true rings=false rotation=42274 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -589874_5099752_2648961 -589876_5099752_2648973 type=ice mass=0.12951479986613348 radius=0.5502112106561361 gravity=43 pressure=38 tempK=98 oxygen=false locked=false rings=false rotation=27998 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -589874_5099752_2648961 -590614_5099752_2650343 type=barren mass=0.010025541386882149 radius=0.30302770646596944 gravity=11 pressure=1 tempK=11 oxygen=false locked=false rings=false rotation=35830 metallicity=1.386534569356337 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 115515_4884922_-1449848 115515_4884922_-1449848 type=ice mass=17.755262406742364 radius=2.182709991409553 gravity=373 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=11557 metallicity=0.8748781923717313 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1339940_-2645564_6560548 type=ice mass=0.2298729667640338 radius=0.6494371879768814 gravity=55 pressure=342 tempK=48 oxygen=false locked=false rings=false rotation=29397 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340044_-2645561_6560536 type=icegiant mass=39.43566987337176 radius=4.438597591383325 gravity=200 pressure=1600 tempK=161 oxygen=false locked=false rings=false rotation=5168 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340052_-2645562_6560553 type=barren mass=0.009480195427145073 radius=0.27373337680420534 gravity=13 pressure=0 tempK=164 oxygen=false locked=true rings=false rotation=84725 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340053_-2645562_6560556 type=superearth mass=6.231484393967069 radius=1.6681649801111824 gravity=224 pressure=1600 tempK=479 oxygen=false locked=true rings=false rotation=50375 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340056_-2645562_6560558 type=lava mass=0.08537137885037245 radius=0.5029534921198497 gravity=34 pressure=0 tempK=963 oxygen=false locked=true rings=false rotation=11637 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340057_-2645562_6560559 type=barren mass=0.004392948489218785 radius=0.22651086590250133 gravity=9 pressure=0 tempK=362 oxygen=false locked=true rings=false rotation=32902 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340058_-2645562_6560557 type=barren mass=0.0074988963088979815 radius=0.2522099109479354 gravity=12 pressure=0 tempK=288 oxygen=false locked=true rings=false rotation=32275 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340061_-2645562_6560554 type=barren mass=0.015936625855832536 radius=0.3457855663938515 gravity=13 pressure=0 tempK=164 oxygen=false locked=true rings=false rotation=25624 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340064_-2645562_6560567 type=gasgiant mass=102.68767886164126 radius=6.729055091962263 gravity=227 pressure=1600 tempK=237 oxygen=false locked=false rings=true rotation=6932 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340073_-2645562_6560550 type=ice mass=1.336800224292926 radius=1.1216746451827144 gravity=106 pressure=1600 tempK=175 oxygen=false locked=false rings=false rotation=75775 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340102_-2645562_6560536 type=ice mass=0.008509153434487935 radius=0.26426323362501386 gravity=12 pressure=2 tempK=47 oxygen=false locked=false rings=false rotation=17614 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1340056_-2645562_6560558 1340119_-2645561_6560521 type=ice mass=15.11240740295715 radius=2.198756624518602 gravity=313 pressure=1600 tempK=89 oxygen=false locked=false rings=false rotation=25680 metallicity=0.578501592932787 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 136345_2380618_4435608 136345_2380618_4435608 type=barren mass=0.10939533716814845 radius=0.5726161013605666 gravity=33 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=12221 metallicity=0.8226319075087267 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2294785_6087239_1852785 2294785_6087239_1852785 type=barren mass=0.02103570917361627 radius=0.3788569313823317 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=72998 metallicity=1.2429109438565753 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2324700_3395074_-773152 2324700_3395074_-773152 type=barren mass=0.013177819722791482 radius=0.32820893486546776 gravity=12 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=25640 metallicity=1.0417666043031906 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2380220_-2985326_2261989 2380220_-2985326_2261989 type=barren mass=0.3739224731395404 radius=0.8176934272940033 gravity=56 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=13304 metallicity=0.8789614972482421 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2455719_-1575919_-1684641 2455719_-1575919_-1684641 type=barren mass=0.004126879491793441 radius=0.23495795814274667 gravity=7 pressure=0 tempK=18 oxygen=false locked=false rings=false rotation=13749 metallicity=1.1779993569874176 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4041882_708425_6811475 4041882_708425_6811475 type=ice mass=1.147869400459926 radius=1.029137364803036 gravity=108 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=26087 metallicity=0.7800850208846841 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4374645_5543557_6609018 4374645_5543557_6609018 type=ice mass=1.4116395069521956 radius=1.0349140608384135 gravity=132 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=90951 metallicity=0.3908883587806408 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4760881_3403866_-1378830 4760881_3403866_-1378830 type=ice mass=2.5382063019272625 radius=1.244696857260704 gravity=164 pressure=0 tempK=40 oxygen=false locked=false rings=false rotation=44999 metallicity=0.9210678128806111 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5532167_-2797664_-2258523 5532167_-2797664_-2258523 type=barren mass=0.04763152630952036 radius=0.41943619401321175 gravity=27 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=29395 metallicity=1.514197594743003 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5628766_5108790_338559 5628766_5108790_338559 type=ice mass=0.008627549469998762 radius=0.28277463573335193 gravity=11 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=70414 metallicity=1.1612566356055734 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5670369_-3422417_6764355 5670369_-3422417_6764355 type=ice mass=1.2943415034829941 radius=1.0232019832097992 gravity=124 pressure=0 tempK=37 oxygen=false locked=false rings=false rotation=6144 metallicity=1.3259025880267747 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5876998_3296879_3027393 5876998_3296879_3027393 type=ice mass=17.45615122214254 radius=2.0861890597953177 gravity=400 pressure=0 tempK=50 oxygen=false locked=false rings=false rotation=32628 metallicity=1.2288308932200982 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6068647_-3169217_818787 6068647_-3169217_818787 type=barren mass=0.04958438430747336 radius=0.4506215156332336 gravity=24 pressure=0 tempK=25 oxygen=false locked=false rings=false rotation=46987 metallicity=1.1250270844255645 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 668050_2680028_1601017 668050_2680028_1601017 type=ice mass=3.5946845711133975 radius=1.3571907745157803 gravity=195 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=13317 metallicity=1.2317969031138332 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6948671_5049241_-2964475 6948671_5049241_-2964475 type=barren mass=0.010701019301469336 radius=0.3116116058746792 gravity=11 pressure=0 tempK=20 oxygen=false locked=false rings=false rotation=14306 metallicity=0.6060910459340265 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 728798_4100023_3876685 728752_4100025_3876660 type=gasgiant mass=219.7087536373542 radius=9.366458180023432 gravity=250 pressure=1600 tempK=117 oxygen=false locked=false rings=true rotation=14162 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 728798_4100023_3876685 728795_4100023_3876676 type=gasgiant mass=114.34606877425351 radius=7.051145770054424 gravity=230 pressure=1600 tempK=272 oxygen=false locked=false rings=true rotation=12235 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 728798_4100023_3876685 728798_4100023_3876682 type=greenhouse mass=8.281666049139647 radius=1.81070986321312 gravity=253 pressure=1600 tempK=441 oxygen=false locked=true rings=false rotation=36126 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 728798_4100023_3876685 728798_4100023_3876685 type=lava mass=8.73584407353523 radius=1.8377454269403521 gravity=259 pressure=237 tempK=1412 oxygen=false locked=true rings=false rotation=34308 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 728798_4100023_3876685 728802_4100023_3876688 type=superearth mass=3.0042019853430992 radius=1.269435908311628 gravity=186 pressure=1596 tempK=403 oxygen=false locked=true rings=false rotation=42325 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 728798_4100023_3876685 728826_4100019_3876763 type=superearth mass=20.75894978211792 radius=2.358873908788583 gravity=373 pressure=1600 tempK=101 oxygen=false locked=false rings=false rotation=43289 metallicity=0.45421203353324946 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1295452_590737_6017888 id=-871681409 kind=STAR name=PGS--3525313.0.3525313 starTemp=150 starSize=1.656844139099121 - system -1420862_99037_1766933 id=-280597797 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless - system -1488035_-3034313_-3156807 id=-401453429 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless - system -2011949_3789060_5390006 id=-66168209 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless - system -268539_1650411_-2625440 id=-1871662501 kind=ROGUE_PLANET name=PGR--3525313.0.-3525313 starless - system -3063801_-759422_5862386 id=-1332799893 kind=STAR name=PGS--3525313.-3525313.3525313 starTemp=70 starSize=1.0023287534713745 - system -3359999_5029417_-1201971 id=-63962517 kind=ROGUE_PLANET name=PGR--3525313.3525313.-3525313 starless - system -464282_-3293100_220531 id=-1130433613 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless - system -589874_5099752_2648961 id=-573199273 kind=STAR name=PGS--3525313.3525313.0 starTemp=40 starSize=0.812318742275238 - system 115515_4884922_-1449848 id=-1386063681 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless - system 1340056_-2645562_6560558 id=-1921583641 kind=STAR name=PGS-0.-3525313.3525313 starTemp=40 starSize=0.7821594476699829 - system 136345_2380618_4435608 id=-655881041 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless - system 2294785_6087239_1852785 id=-747899749 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless - system 2324700_3395074_-773152 id=-333451093 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless - system 2380220_-2985326_2261989 id=-846895665 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless - system 2455719_-1575919_-1684641 id=-1093289653 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless - system 4041882_708425_6811475 id=-1527098829 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless - system 4374645_5543557_6609018 id=-138926061 kind=ROGUE_PLANET name=PGR-3525313.3525313.3525313 starless - system 4760881_3403866_-1378830 id=-1839995337 kind=ROGUE_PLANET name=PGR-3525313.0.-3525313 starless - system 5532167_-2797664_-2258523 id=-850423465 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless - system 5628766_5108790_338559 id=-1665662161 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless - system 5670369_-3422417_6764355 id=-862267757 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless - system 5876998_3296879_3027393 id=-286545925 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless - system 6068647_-3169217_818787 id=-621289557 kind=ROGUE_PLANET name=PGR-3525313.-3525313.0 starless - system 668050_2680028_1601017 id=-1525225641 kind=ROGUE_PLANET name=PGR-0.0.0 starless - system 6948671_5049241_-2964475 id=-162398185 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless - system 728798_4100023_3876685 id=-1903899713 kind=STAR name=PGS-0.3525313.3525313 starTemp=40 starSize=0.8625843524932861 -seed 2147483647 systems=27 - body -1396847_-3424625_-1950936 -1396847_-3424625_-1950936 kind=MOON orbit=0 radius=0.380681397036819 starId=-207345417 frame=false at=-14891,0,-53569 - body -1396847_-3424625_-1950936 -1396847_-3424625_-1950936 kind=MOON orbit=0 radius=0.38228172882424566 starId=-207345417 frame=false at=28188,0,-22393 - body -1396847_-3424625_-1950936 -1396847_-3424625_-1950936 kind=ROGUE_PLANET orbit=0 radius=0.20025427508835883 starId=-207345417 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166603_6365360_-3072722 kind=ASTEROID_BELT orbit=659 radius=0.0 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166687_6365359_-3072746 kind=GAS_GIANT orbit=234 radius=6.27869866603346 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=GAS_GIANT orbit=412 radius=8.89748111132246 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=MOON orbit=412 radius=0.47056308784713413 starId=-1917888101 frame=false at=-1133479,0,-2473895 - body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=MOON orbit=412 radius=0.5454668740385029 starId=-1917888101 frame=false at=-603139,0,-774443 - body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=MOON orbit=412 radius=0.6110944197764181 starId=-1917888101 frame=false at=-2576920,0,-420480 - body -166709_6365358_-3072784 -166690_6365359_-3072709 kind=MOON orbit=412 radius=0.7214649640195177 starId=-1917888101 frame=false at=547510,0,-590406 - body -166709_6365358_-3072784 -166690_6365359_-3072797 kind=PLANET orbit=125 radius=1.250836965969712 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166705_6365358_-3072786 kind=ASTEROID_BELT orbit=26 radius=0.0 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166709_6365358_-3072784 kind=STAR orbit=0 radius=0.0 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=GAS_GIANT orbit=48 radius=6.258748728626493 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.26863908701994194 starId=-1917888101 frame=false at=698868,0,1262013 - body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.3051035532919284 starId=-1917888101 frame=false at=425871,0,-656818 - body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.3611089191625616 starId=-1917888101 frame=false at=-599683,0,-1775882 - body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.5302972455859541 starId=-1917888101 frame=false at=-781246,0,-104467 - body -166709_6365358_-3072784 -166710_6365358_-3072775 kind=MOON orbit=48 radius=0.6445900164109255 starId=-1917888101 frame=false at=-1051155,0,-488690 - body -166709_6365358_-3072784 -166710_6365358_-3072783 kind=MOON orbit=6 radius=0.27729654812010507 starId=-1917888101 frame=false at=99577,0,135060 - body -166709_6365358_-3072784 -166710_6365358_-3072783 kind=MOON orbit=6 radius=0.662969993722506 starId=-1917888101 frame=false at=-127913,0,-40595 - body -166709_6365358_-3072784 -166710_6365358_-3072783 kind=PLANET orbit=6 radius=0.7448218246108711 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166710_6365358_-3072786 kind=MOON orbit=12 radius=0.2516749893912134 starId=-1917888101 frame=false at=103027,0,102599 - body -166709_6365358_-3072784 -166710_6365358_-3072786 kind=MOON orbit=12 radius=0.5934694065660353 starId=-1917888101 frame=false at=-115030,0,-6257 - body -166709_6365358_-3072784 -166710_6365358_-3072786 kind=PLANET orbit=12 radius=0.5989009697838469 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166713_6365358_-3072782 kind=MOON orbit=22 radius=0.5660636212701569 starId=-1917888101 frame=false at=-19391,0,163051 - body -166709_6365358_-3072784 -166713_6365358_-3072782 kind=PLANET orbit=22 radius=1.3550701895269492 starId=-1917888101 frame=true at=0,0,0 - body -166709_6365358_-3072784 -166719_6365357_-3072772 kind=GAS_GIANT orbit=83 radius=5.854149891764633 starId=-1917888101 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798630_5881270_282302 kind=PLANET orbit=453 radius=0.48679028766847965 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798643_5881263_282220 kind=ASTEROID_BELT orbit=724 radius=0.0 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798701_5881269_282360 kind=PLANET orbit=113 radius=0.36674069378182417 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798702_5881269_282329 kind=GAS_GIANT orbit=63 radius=6.549907562151851 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798702_5881269_282329 kind=MOON orbit=63 radius=0.2150038482907262 starId=-1883424565 frame=false at=751272,0,529640 - body -1798706_5881269_282340 -1798702_5881269_282329 kind=MOON orbit=63 radius=0.27632207208413984 starId=-1883424565 frame=false at=432230,0,-80156 - body -1798706_5881269_282340 -1798702_5881269_282329 kind=MOON orbit=63 radius=0.3934982191685314 starId=-1883424565 frame=false at=-125888,0,1192574 - body -1798706_5881269_282340 -1798702_5881269_282329 kind=MOON orbit=63 radius=0.45452060090162344 starId=-1883424565 frame=false at=919740,0,1417331 - body -1798706_5881269_282340 -1798704_5881269_282340 kind=PLANET orbit=10 radius=1.262262105304908 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798704_5881269_282342 kind=PLANET orbit=14 radius=1.2471740000967104 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798705_5881269_282345 kind=GAS_GIANT orbit=27 radius=4.113660120859359 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798705_5881269_282345 kind=MOON orbit=27 radius=0.5031994937307466 starId=-1883424565 frame=false at=801138,0,414465 - body -1798706_5881269_282340 -1798706_5881269_282337 kind=ASTEROID_BELT orbit=15 radius=0.0 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798706_5881269_282340 kind=STAR orbit=0 radius=0.0 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798715_5881267_282297 kind=MOON orbit=234 radius=0.6837647158758842 starId=-1883424565 frame=false at=23543,0,179261 - body -1798706_5881269_282340 -1798715_5881267_282297 kind=PLANET orbit=234 radius=0.6754460606140396 starId=-1883424565 frame=true at=0,0,0 - body -1798706_5881269_282340 -1798889_5881269_281652 kind=STAR orbit=3808 radius=77.03123949766159 starId=-1883424566 frame=true at=0,0,0 - body -1842127_-2662654_258460 -1842127_-2662654_258460 kind=MOON orbit=0 radius=0.3259772767413664 starId=-1529010057 frame=false at=-123991,0,-104261 - body -1842127_-2662654_258460 -1842127_-2662654_258460 kind=ROGUE_PLANET orbit=0 radius=2.2979679988747774 starId=-1529010057 frame=true at=0,0,0 - body -2750569_1404918_6927999 -2750569_1404918_6927999 kind=ROGUE_PLANET orbit=0 radius=2.265099431122441 starId=-455553521 frame=true at=0,0,0 - body -2794652_-893855_4236484 -2794640_-893853_4236525 kind=PLANET orbit=228 radius=1.0584082441002571 starId=-1572202913 frame=true at=0,0,0 - body -2794652_-893855_4236484 -2794645_-893855_4236486 kind=GAS_GIANT orbit=38 radius=6.950995980399325 starId=-1572202913 frame=true at=0,0,0 - body -2794652_-893855_4236484 -2794645_-893855_4236486 kind=MOON orbit=38 radius=0.2204833365364542 starId=-1572202913 frame=false at=1227764,0,1535753 - body -2794652_-893855_4236484 -2794645_-893855_4236486 kind=MOON orbit=38 radius=0.3358860955076055 starId=-1572202913 frame=false at=-514825,0,428784 - body -2794652_-893855_4236484 -2794645_-893855_4236486 kind=MOON orbit=38 radius=0.52482374499012 starId=-1572202913 frame=false at=422757,0,288117 - body -2794652_-893855_4236484 -2794649_-893858_4236552 kind=ASTEROID_BELT orbit=364 radius=0.0 starId=-1572202913 frame=true at=0,0,0 - body -2794652_-893855_4236484 -2794652_-893855_4236484 kind=STAR orbit=0 radius=0.0 starId=-1572202913 frame=true at=0,0,0 - body -2794652_-893855_4236484 -2794653_-893855_4236482 kind=MOON orbit=9 radius=0.7423671939218839 starId=-1572202913 frame=false at=-31968,0,-35268 - body -2794652_-893855_4236484 -2794653_-893855_4236482 kind=PLANET orbit=9 radius=0.500787448791386 starId=-1572202913 frame=true at=0,0,0 - body -2794652_-893855_4236484 -2794655_-893855_4236482 kind=ASTEROID_BELT orbit=21 radius=0.0 starId=-1572202913 frame=true at=0,0,0 - body -2976937_6586333_3889163 -2976937_6586333_3889163 kind=ROGUE_PLANET orbit=0 radius=1.0532820872104762 starId=-1838315877 frame=true at=0,0,0 - body -3311706_3302146_-2970565 -3311663_3302148_-2970593 kind=MOON orbit=276 radius=0.554297739536862 starId=-36122729 frame=false at=297632,0,-304781 - body -3311706_3302146_-2970565 -3311663_3302148_-2970593 kind=MOON orbit=276 radius=0.6619246899853962 starId=-36122729 frame=false at=-243712,0,-394372 - body -3311706_3302146_-2970565 -3311663_3302148_-2970593 kind=PLANET orbit=276 radius=2.4156495201898482 starId=-36122729 frame=true at=0,0,0 - body -3311706_3302146_-2970565 -3311670_3302148_-2970491 kind=ASTEROID_BELT orbit=441 radius=0.0 starId=-36122729 frame=true at=0,0,0 - body -3311706_3302146_-2970565 -3311699_3302146_-2970539 kind=MOON orbit=142 radius=0.26316241137208385 starId=-36122729 frame=false at=62657,0,-100462 - body -3311706_3302146_-2970565 -3311699_3302146_-2970539 kind=PLANET orbit=142 radius=0.7233642449067668 starId=-36122729 frame=true at=0,0,0 - body -3311706_3302146_-2970565 -3311706_3302146_-2970565 kind=STAR orbit=0 radius=0.0 starId=-36122729 frame=true at=0,0,0 - body -3311706_3302146_-2970565 -3311707_3302146_-2970562 kind=MOON orbit=17 radius=0.42420342171926473 starId=-36122729 frame=false at=164654,0,453005 - body -3311706_3302146_-2970565 -3311707_3302146_-2970562 kind=PLANET orbit=17 radius=1.9825878133469808 starId=-36122729 frame=true at=0,0,0 - body -3311706_3302146_-2970565 -3311707_3302146_-2970563 kind=MOON orbit=12 radius=0.4190719027416059 starId=-36122729 frame=false at=141189,0,459184 - body -3311706_3302146_-2970565 -3311707_3302146_-2970563 kind=PLANET orbit=12 radius=1.9919617781749086 starId=-36122729 frame=true at=0,0,0 - body -3311706_3302146_-2970565 -3311714_3302146_-2970567 kind=MOON orbit=45 radius=0.7298812093705165 starId=-36122729 frame=false at=-75971,0,2090 - body -3311706_3302146_-2970565 -3311714_3302146_-2970567 kind=PLANET orbit=45 radius=0.6237393525805972 starId=-36122729 frame=true at=0,0,0 - body -3311706_3302146_-2970565 -3330110_3302146_-2976634 kind=STAR orbit=103632 radius=82.97344805538654 starId=-36122730 frame=true at=0,0,0 - body -671411_3391485_519485 -671411_3391485_519485 kind=MOON orbit=0 radius=1.645018902035351 starId=-1864177861 frame=false at=49614,0,125983 - body -671411_3391485_519485 -671411_3391485_519485 kind=ROGUE_PLANET orbit=0 radius=0.6047082686992997 starId=-1864177861 frame=true at=0,0,0 - body 1594627_-205296_2117674 1594572_-205298_2117593 kind=ASTEROID_BELT orbit=523 radius=0.0 starId=-1076464245 frame=true at=0,0,0 - body 1594627_-205296_2117674 1594577_-205297_2117639 kind=PLANET orbit=327 radius=1.4294073187663368 starId=-1076464245 frame=true at=0,0,0 - body 1594627_-205296_2117674 1594618_-205296_2117664 kind=PLANET orbit=69 radius=1.6318762672113716 starId=-1076464245 frame=true at=0,0,0 - body 1594627_-205296_2117674 1594625_-205296_2117675 kind=MOON orbit=11 radius=0.21746637483760253 starId=-1076464245 frame=false at=119796,0,-297809 - body 1594627_-205296_2117674 1594625_-205296_2117675 kind=PLANET orbit=11 radius=2.1060434538482697 starId=-1076464245 frame=true at=0,0,0 - body 1594627_-205296_2117674 1594627_-205296_2117674 kind=STAR orbit=0 radius=0.0 starId=-1076464245 frame=true at=0,0,0 - body 1763248_3924110_-951397 1763248_3924110_-951397 kind=MOON orbit=0 radius=0.8915460990645903 starId=-965124545 frame=false at=134187,0,47060 - body 1763248_3924110_-951397 1763248_3924110_-951397 kind=MOON orbit=0 radius=2.298004836008735 starId=-965124545 frame=false at=45410,0,-35760 - body 1763248_3924110_-951397 1763248_3924110_-951397 kind=ROGUE_PLANET orbit=0 radius=0.4785825282492802 starId=-965124545 frame=true at=0,0,0 - body 1859067_623717_-2088070 1859067_623717_-2088070 kind=ROGUE_PLANET orbit=0 radius=0.3155669885034604 starId=-1701373473 frame=true at=0,0,0 - body 1889446_813181_4205893 1889374_813177_4205834 kind=MOON orbit=500 radius=0.41096038094798665 starId=-1019483873 frame=false at=-283871,0,265381 - body 1889446_813181_4205893 1889374_813177_4205834 kind=PLANET orbit=500 radius=1.3029574931420822 starId=-1019483873 frame=true at=0,0,0 - body 1889446_813181_4205893 1889421_813182_4205913 kind=GAS_GIANT orbit=170 radius=5.529884538397504 starId=-1019483873 frame=true at=0,0,0 - body 1889446_813181_4205893 1889421_813182_4205913 kind=MOON orbit=170 radius=0.24874093551107213 starId=-1019483873 frame=false at=-938495,0,245210 - body 1889446_813181_4205893 1889421_813182_4205913 kind=MOON orbit=170 radius=0.32834977436085333 starId=-1019483873 frame=false at=-258248,0,-263289 - body 1889446_813181_4205893 1889432_813181_4205891 kind=MOON orbit=78 radius=0.2601505362079251 starId=-1019483873 frame=false at=-10429,0,-12134 - body 1889446_813181_4205893 1889432_813181_4205891 kind=PLANET orbit=78 radius=0.2173409056280414 starId=-1019483873 frame=true at=0,0,0 - body 1889446_813181_4205893 1889438_813181_4205897 kind=GAS_GIANT orbit=46 radius=6.619662381279651 starId=-1019483873 frame=true at=0,0,0 - body 1889446_813181_4205893 1889438_813181_4205897 kind=MOON orbit=46 radius=0.34173610744735333 starId=-1019483873 frame=false at=-1326668,0,-1327810 - body 1889446_813181_4205893 1889438_813181_4205897 kind=MOON orbit=46 radius=0.3484246080267326 starId=-1019483873 frame=false at=-1454045,0,714250 - body 1889446_813181_4205893 1889438_813181_4205897 kind=MOON orbit=46 radius=0.5295264158608558 starId=-1019483873 frame=false at=-581550,0,-553432 - body 1889446_813181_4205893 1889438_813181_4205897 kind=MOON orbit=46 radius=0.659864562789785 starId=-1019483873 frame=false at=898431,0,198045 - body 1889446_813181_4205893 1889441_813181_4205894 kind=ASTEROID_BELT orbit=25 radius=0.0 starId=-1019483873 frame=true at=0,0,0 - body 1889446_813181_4205893 1889442_813181_4205893 kind=MOON orbit=22 radius=0.7169628691821708 starId=-1019483873 frame=false at=9297,0,24495 - body 1889446_813181_4205893 1889442_813181_4205893 kind=PLANET orbit=22 radius=0.3971078292923061 starId=-1019483873 frame=true at=0,0,0 - body 1889446_813181_4205893 1889445_813181_4205892 kind=MOON orbit=7 radius=0.2549195784817597 starId=-1019483873 frame=false at=-228599,0,69738 - body 1889446_813181_4205893 1889445_813181_4205892 kind=PLANET orbit=7 radius=1.458171431777551 starId=-1019483873 frame=true at=0,0,0 - body 1889446_813181_4205893 1889446_813181_4205893 kind=STAR orbit=0 radius=0.0 starId=-1019483873 frame=true at=0,0,0 - body 1889446_813181_4205893 1889549_813175_4206001 kind=ASTEROID_BELT orbit=800 radius=0.0 starId=-1019483873 frame=true at=0,0,0 - body 2198808_-575566_6365497 2198808_-575566_6365497 kind=ROGUE_PLANET orbit=0 radius=1.905262701085582 starId=-1785782589 frame=true at=0,0,0 - body 2233013_199342_2539057 2232959_199344_2538990 kind=ASTEROID_BELT orbit=459 radius=0.0 starId=-1579160837 frame=true at=0,0,0 - body 2233013_199342_2539057 2233004_199342_2539063 kind=MOON orbit=57 radius=0.28074740541664756 starId=-1579160837 frame=false at=-235858,0,-208797 - body 2233013_199342_2539057 2233004_199342_2539063 kind=PLANET orbit=57 radius=1.1851610699153496 starId=-1579160837 frame=true at=0,0,0 - body 2233013_199342_2539057 2233011_199342_2539058 kind=MOON orbit=9 radius=0.442848895606726 starId=-1579160837 frame=false at=111885,0,-188495 - body 2233013_199342_2539057 2233011_199342_2539058 kind=PLANET orbit=9 radius=2.4364227035505546 starId=-1579160837 frame=true at=0,0,0 - body 2233013_199342_2539057 2233013_199342_2539057 kind=STAR orbit=0 radius=0.0 starId=-1579160837 frame=true at=0,0,0 - body 2233013_199342_2539057 2233058_199344_2539027 kind=MOON orbit=287 radius=0.24108083163357644 starId=-1579160837 frame=false at=-25401,0,-49459 - body 2233013_199342_2539057 2233058_199344_2539027 kind=PLANET orbit=287 radius=0.2667562435907831 starId=-1579160837 frame=true at=0,0,0 - body 2467378_-3394549_-3171449 2467378_-3394549_-3171449 kind=MOON orbit=0 radius=1.0754090655621613 starId=-1420637497 frame=false at=117078,0,175538 - body 2467378_-3394549_-3171449 2467378_-3394549_-3171449 kind=ROGUE_PLANET orbit=0 radius=2.356867373565178 starId=-1420637497 frame=true at=0,0,0 - body 3890455_2004932_-233592 3889392_2004932_-229119 kind=STAR orbit=24586 radius=103.60897379100322 starId=-1175248790 frame=true at=0,0,0 - body 3890455_2004932_-233592 3890445_2004933_-233643 kind=MOON orbit=279 radius=0.5931194317902839 starId=-1175248789 frame=false at=-53650,0,-4009 - body 3890455_2004932_-233592 3890445_2004933_-233643 kind=MOON orbit=279 radius=0.7125636608885282 starId=-1175248789 frame=false at=-43810,0,15883 - body 3890455_2004932_-233592 3890445_2004933_-233643 kind=PLANET orbit=279 radius=0.23580782176504658 starId=-1175248789 frame=true at=0,0,0 - body 3890455_2004932_-233592 3890454_2004932_-233594 kind=MOON orbit=9 radius=0.5131233425406025 starId=-1175248789 frame=false at=366115,0,-71336 - body 3890455_2004932_-233592 3890454_2004932_-233594 kind=MOON orbit=9 radius=0.6584912217257421 starId=-1175248789 frame=false at=258737,0,86460 - body 3890455_2004932_-233592 3890454_2004932_-233594 kind=PLANET orbit=9 radius=1.2566710178770715 starId=-1175248789 frame=true at=0,0,0 - body 3890455_2004932_-233592 3890455_2004932_-233592 kind=STAR orbit=0 radius=0.0 starId=-1175248789 frame=true at=0,0,0 - body 3890455_2004932_-233592 3890463_2004931_-233509 kind=ASTEROID_BELT orbit=446 radius=0.0 starId=-1175248789 frame=true at=0,0,0 - body 3890455_2004932_-233592 3890469_2004932_-233599 kind=PLANET orbit=83 radius=2.2796306052080975 starId=-1175248789 frame=true at=0,0,0 - body 3996531_-3038111_5303430 3996531_-3038111_5303430 kind=ROGUE_PLANET orbit=0 radius=1.9119707660338916 starId=-689584929 frame=true at=0,0,0 - body 4299007_3832131_-2532498 4299007_3832131_-2532498 kind=MOON orbit=0 radius=1.423539876399934 starId=-657806589 frame=false at=192512,0,-334105 - body 4299007_3832131_-2532498 4299007_3832131_-2532498 kind=MOON orbit=0 radius=1.821387511515449 starId=-657806589 frame=false at=-426105,0,-161828 - body 4299007_3832131_-2532498 4299007_3832131_-2532498 kind=ROGUE_PLANET orbit=0 radius=1.8899347763673122 starId=-657806589 frame=true at=0,0,0 - body 5022771_6570089_4140118 5022771_6570089_4140118 kind=ROGUE_PLANET orbit=0 radius=2.2101822535923454 starId=-1402022073 frame=true at=0,0,0 - body 5089392_-2456012_2414774 5089392_-2456012_2414774 kind=ROGUE_PLANET orbit=0 radius=0.7837832198512151 starId=-474729317 frame=true at=0,0,0 - body 5718025_-888233_-3341159 5718020_-888233_-3341168 kind=STAR orbit=57 radius=76.89058984816074 starId=-1451948175 frame=true at=0,0,0 - body 5718025_-888233_-3341159 5718023_-888233_-3341159 kind=GAS_GIANT orbit=13 radius=7.717711568622234 starId=-1451948173 frame=true at=0,0,0 - body 5718025_-888233_-3341159 5718024_-888233_-3341159 kind=ASTEROID_BELT orbit=7 radius=0.0 starId=-1451948173 frame=true at=0,0,0 - body 5718025_-888233_-3341159 5718025_-888233_-3341159 kind=STAR orbit=0 radius=0.0 starId=-1451948173 frame=true at=0,0,0 - body 5718025_-888233_-3341159 5718075_-888232_-3341159 kind=MOON orbit=267 radius=0.6443974314996002 starId=-1451948173 frame=false at=60835,0,-19877 - body 5718025_-888233_-3341159 5718075_-888232_-3341159 kind=PLANET orbit=267 radius=0.2814384436049694 starId=-1451948173 frame=true at=0,0,0 - body 5718025_-888233_-3341159 5718083_-888231_-3341214 kind=ASTEROID_BELT orbit=427 radius=0.0 starId=-1451948173 frame=true at=0,0,0 - body 5718025_-888233_-3341159 5718496_-888233_-3340282 kind=STAR orbit=5324 radius=79.54021711528301 starId=-1451948174 frame=true at=0,0,0 - body 5730187_2991355_6930094 5730187_2991355_6930094 kind=ROGUE_PLANET orbit=0 radius=0.3614012444688947 starId=-940407273 frame=true at=0,0,0 - body 6244948_3150169_3269973 6244948_3150169_3269973 kind=ROGUE_PLANET orbit=0 radius=0.9233026667448736 starId=-1552747849 frame=true at=0,0,0 - body 6537086_5282959_646441 6537086_5282959_646441 kind=MOON orbit=0 radius=0.20551020117068142 starId=-715739421 frame=false at=-3212,0,-194173 - body 6537086_5282959_646441 6537086_5282959_646441 kind=ROGUE_PLANET orbit=0 radius=0.9432117495314174 starId=-715739421 frame=true at=0,0,0 - body 713381_4456830_2132934 713373_4456830_2132927 kind=PLANET orbit=55 radius=1.0653947740642289 starId=-373901957 frame=true at=0,0,0 - body 713381_4456830_2132934 713374_4456833_2133007 kind=ASTEROID_BELT orbit=393 radius=0.0 starId=-373901957 frame=true at=0,0,0 - body 713381_4456830_2132934 713381_4456830_2132934 kind=STAR orbit=0 radius=0.0 starId=-373901957 frame=true at=0,0,0 - body 713381_4456830_2132934 713383_4456830_2132933 kind=PLANET orbit=13 radius=0.20757672260460924 starId=-373901957 frame=true at=0,0,0 - body 713381_4456830_2132934 713393_4456829_2132978 kind=PLANET orbit=246 radius=0.4310713080560144 starId=-373901957 frame=true at=0,0,0 - body 713381_4456830_2132934 714948_4456830_2142291 kind=STAR orbit=50736 radius=80.36679978132248 starId=-373901958 frame=true at=0,0,0 - body 816822_6851185_5059304 814131_6851104_5058681 kind=ASTEROID_BELT orbit=14779 radius=0.0 starId=-547337629 frame=true at=0,0,0 - body 816822_6851185_5059304 815136_6851223_5058930 kind=PLANET orbit=9237 radius=1.5437826210985488 starId=-547337629 frame=true at=0,0,0 - body 816822_6851185_5059304 816691_6851184_5059714 kind=GAS_GIANT orbit=2301 radius=5.685540056018066 starId=-547337629 frame=true at=0,0,0 - body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.26660902700499145 starId=-547337629 frame=false at=802878,0,317020 - body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.4030505774414985 starId=-547337629 frame=false at=-83132,0,911818 - body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.4860389608165845 starId=-547337629 frame=false at=908915,0,-412162 - body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.6472653505203445 starId=-547337629 frame=false at=309950,0,-703551 - body 816822_6851185_5059304 816691_6851184_5059714 kind=MOON orbit=2301 radius=0.713368566614681 starId=-547337629 frame=false at=71318,0,-615482 - body 816822_6851185_5059304 816711_6851188_5059282 kind=PLANET orbit=606 radius=1.7606471887988784 starId=-547337629 frame=true at=0,0,0 - body 816822_6851185_5059304 816817_6851195_5059543 kind=ASTEROID_BELT orbit=1278 radius=0.0 starId=-547337629 frame=true at=0,0,0 - body 816822_6851185_5059304 816818_6851185_5059302 kind=STAR orbit=25 radius=105.6524378335476 starId=-547337630 frame=true at=0,0,0 - body 816822_6851185_5059304 816822_6851185_5059304 kind=STAR orbit=0 radius=0.0 starId=-547337629 frame=true at=0,0,0 - body 816822_6851185_5059304 816843_6851183_5059334 kind=PLANET orbit=199 radius=0.4210403808014127 starId=-547337629 frame=true at=0,0,0 - derived -1396847_-3424625_-1950936 -1396847_-3424625_-1950936 type=ice mass=0.0022618512298017263 radius=0.20025427508835883 gravity=6 pressure=0 tempK=17 oxygen=false locked=false rings=false rotation=73211 metallicity=0.4737519889283227 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166603_6365360_-3072722 type=ice mass=3.1151188881989325 radius=1.3556723909355317 gravity=169 pressure=1600 tempK=65 oxygen=false locked=false rings=false rotation=12468 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166687_6365359_-3072746 type=icegiant mass=87.56370257055207 radius=6.27869866603346 gravity=222 pressure=1600 tempK=115 oxygen=false locked=false rings=true rotation=7535 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166690_6365359_-3072709 type=gasgiant mass=195.22625166955882 radius=8.89748111132246 gravity=247 pressure=1600 tempK=87 oxygen=false locked=false rings=true rotation=10198 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166690_6365359_-3072797 type=ice mass=2.8144803726125183 radius=1.250836965969712 gravity=180 pressure=1600 tempK=149 oxygen=false locked=false rings=false rotation=81603 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166705_6365358_-3072786 type=greenhouse mass=18.475531310837795 radius=2.0737910484877493 gravity=400 pressure=1600 tempK=292 oxygen=false locked=true rings=false rotation=53087 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166709_6365358_-3072784 type=lava mass=6.255313763446167 radius=1.617691722855842 gravity=239 pressure=300 tempK=1350 oxygen=false locked=true rings=false rotation=6526 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166710_6365358_-3072775 type=gasgiant mass=86.9251064798208 radius=6.258748728626493 gravity=222 pressure=1600 tempK=255 oxygen=false locked=false rings=true rotation=4921 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166710_6365358_-3072783 type=barren mass=0.38171939719307657 radius=0.7448218246108711 gravity=69 pressure=20 tempK=370 oxygen=false locked=true rings=false rotation=6022 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166710_6365358_-3072786 type=barren mass=0.17055809087109444 radius=0.5989009697838469 gravity=48 pressure=11 tempK=261 oxygen=false locked=true rings=false rotation=7031 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166713_6365358_-3072782 type=superearth mass=3.6561026253538085 radius=1.3550701895269492 gravity=199 pressure=1600 tempK=410 oxygen=false locked=true rings=false rotation=19243 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -166709_6365358_-3072784 -166719_6365357_-3072772 type=icegiant mass=74.54023741745952 radius=5.854149891764633 gravity=218 pressure=1600 tempK=194 oxygen=false locked=false rings=true rotation=7786 metallicity=1.4603123398035485 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798630_5881270_282302 type=ice mass=0.07138251152104098 radius=0.48679028766847965 gravity=30 pressure=82 tempK=43 oxygen=false locked=false rings=false rotation=12463 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798643_5881263_282220 type=superearth mass=3.539246956665589 radius=1.4754035474396239 gravity=163 pressure=1600 tempK=83 oxygen=false locked=false rings=false rotation=17413 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798701_5881269_282360 type=barren mass=0.028018407051957496 radius=0.36674069378182417 gravity=21 pressure=5 tempK=99 oxygen=false locked=false rings=false rotation=10798 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798702_5881269_282329 type=gasgiant mass=96.50833814034614 radius=6.549907562151851 gravity=225 pressure=1600 tempK=259 oxygen=false locked=false rings=false rotation=5512 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798704_5881269_282340 type=greenhouse mass=2.4094719316185538 radius=1.262262105304908 gravity=151 pressure=330 tempK=368 oxygen=false locked=true rings=false rotation=64562 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798704_5881269_282342 type=greenhouse mass=2.3106388878060296 radius=1.2471740000967104 gravity=149 pressure=476 tempK=341 oxygen=false locked=true rings=false rotation=65521 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798705_5881269_282345 type=gasgiant mass=33.109245123445625 radius=4.113660120859359 gravity=196 pressure=1600 tempK=396 oxygen=false locked=false rings=false rotation=7607 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798706_5881269_282337 type=desert mass=0.2752602140278586 radius=0.698650047123628 gravity=56 pressure=4 tempK=256 oxygen=false locked=true rings=false rotation=8809 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798706_5881269_282340 type=barren mass=0.3016170857300653 radius=0.7153890467213351 gravity=59 pressure=0 tempK=1053 oxygen=false locked=true rings=false rotation=8490 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798715_5881267_282297 type=ice mass=0.21095799019536213 radius=0.6754460606140396 gravity=46 pressure=380 tempK=88 oxygen=false locked=false rings=false rotation=7985 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1798706_5881269_282340 -1798889_5881269_281652 type=ice mass=0.0032045993022915427 radius=0.21220936065605298 gravity=7 pressure=0 tempK=14 oxygen=false locked=false rings=false rotation=38739 metallicity=1.4312801186373152 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -1842127_-2662654_258460 -1842127_-2662654_258460 type=superearth mass=18.908576007473027 radius=2.2979679988747774 gravity=358 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=52746 metallicity=0.4083297359078128 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2750569_1404918_6927999 -2750569_1404918_6927999 type=superearth mass=16.537905395105522 radius=2.265099431122441 gravity=322 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=38348 metallicity=1.5119926936005776 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2794652_-893855_4236484 -2794640_-893853_4236525 type=ice mass=1.300859990789865 radius=1.0584082441002571 gravity=116 pressure=1600 tempK=114 oxygen=false locked=false rings=false rotation=11737 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2794652_-893855_4236484 -2794645_-893855_4236486 type=gasgiant mass=110.64508584193437 radius=6.950995980399325 gravity=229 pressure=1600 tempK=296 oxygen=false locked=false rings=false rotation=5885 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2794652_-893855_4236484 -2794649_-893858_4236552 type=gasgiant mass=194.87833912654403 radius=8.890583638719136 gravity=247 pressure=1600 tempK=95 oxygen=false locked=false rings=true rotation=7526 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2794652_-893855_4236484 -2794652_-893855_4236484 type=barren mass=0.21816990027666386 radius=0.7049496928906398 gravity=44 pressure=1 tempK=936 oxygen=false locked=true rings=false rotation=45809 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2794652_-893855_4236484 -2794653_-893855_4236482 type=desert mass=0.06555048019117525 radius=0.500787448791386 gravity=26 pressure=0 tempK=294 oxygen=false locked=true rings=false rotation=27759 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2794652_-893855_4236484 -2794655_-893855_4236482 type=superearth mass=12.865860238956149 radius=2.1363005807875832 gravity=282 pressure=1600 tempK=434 oxygen=false locked=true rings=false rotation=13690 metallicity=0.8945024525980521 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -2976937_6586333_3889163 -2976937_6586333_3889163 type=ice mass=1.2383528736883684 radius=1.0532820872104762 gravity=112 pressure=0 tempK=36 oxygen=false locked=false rings=false rotation=25933 metallicity=0.8120316374256196 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3311706_3302146_-2970565 -3311663_3302148_-2970593 type=ice mass=20.099637089137612 radius=2.4156495201898482 gravity=344 pressure=1600 tempK=105 oxygen=false locked=false rings=false rotation=12879 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3311706_3302146_-2970565 -3311670_3302148_-2970491 type=icegiant mass=88.02595803239399 radius=6.293088411319346 gravity=222 pressure=1600 tempK=87 oxygen=false locked=false rings=true rotation=13644 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3311706_3302146_-2970565 -3311699_3302146_-2970539 type=ice mass=0.3226737910885433 radius=0.7233642449067668 gravity=62 pressure=484 tempK=108 oxygen=false locked=false rings=false rotation=17584 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3311706_3302146_-2970565 -3311706_3302146_-2970565 type=lava mass=3.903512805890654 radius=1.5025979873529536 gravity=173 pressure=128 tempK=1136 oxygen=false locked=true rings=false rotation=30728 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3311706_3302146_-2970565 -3311707_3302146_-2970562 type=superearth mass=11.479345977202328 radius=1.9825878133469808 gravity=292 pressure=1600 tempK=486 oxygen=false locked=true rings=false rotation=78730 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3311706_3302146_-2970565 -3311707_3302146_-2970563 type=greenhouse mass=11.73253610000442 radius=1.9919617781749086 gravity=296 pressure=1600 tempK=447 oxygen=false locked=true rings=false rotation=79313 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3311706_3302146_-2970565 -3311714_3302146_-2970567 type=ice mass=0.16483083171053134 radius=0.6237393525805972 gravity=42 pressure=34 tempK=115 oxygen=false locked=true rings=false rotation=28666 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -3311706_3302146_-2970565 -3330110_3302146_-2976634 type=ice mass=1.9353099849675102 radius=1.1818452121342142 gravity=139 pressure=1600 tempK=5 oxygen=false locked=false rings=false rotation=15563 metallicity=1.0699748387376398 terrain=TerrainOption[NATIVE genType=0 w=1] - derived -671411_3391485_519485 -671411_3391485_519485 type=ice mass=0.17890138579382228 radius=0.6047082686992997 gravity=49 pressure=0 tempK=29 oxygen=false locked=false rings=false rotation=53681 metallicity=0.6063925496141216 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1594627_-205296_2117674 1594572_-205298_2117593 type=gasgiant mass=74.45527086611237 radius=5.851247652687276 gravity=217 pressure=1600 tempK=75 oxygen=false locked=false rings=false rotation=12378 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1594627_-205296_2117674 1594577_-205297_2117639 type=ice mass=3.411046165706922 radius=1.4294073187663368 gravity=167 pressure=1600 tempK=90 oxygen=false locked=false rings=false rotation=12436 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1594627_-205296_2117674 1594618_-205296_2117664 type=superearth mass=5.867304871492831 radius=1.6318762672113716 gravity=220 pressure=1600 tempK=227 oxygen=false locked=false rings=false rotation=26217 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1594627_-205296_2117674 1594625_-205296_2117675 type=superearth mass=14.367808430048353 radius=2.1060434538482697 gravity=324 pressure=1600 tempK=569 oxygen=false locked=true rings=false rotation=80508 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1594627_-205296_2117674 1594627_-205296_2117674 type=barren mass=0.009908214049765357 radius=0.2821097998980815 gravity=12 pressure=0 tempK=889 oxygen=false locked=true rings=false rotation=49947 metallicity=1.434277268575689 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1763248_3924110_-951397 1763248_3924110_-951397 type=barren mass=0.07808937421335045 radius=0.4785825282492802 gravity=34 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=11823 metallicity=0.43939222631776415 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1859067_623717_-2088070 1859067_623717_-2088070 type=barren mass=0.013289739809071662 radius=0.3155669885034604 gravity=13 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=12488 metallicity=1.170886546686213 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1889446_813181_4205893 1889374_813177_4205834 type=ice mass=2.1937532157379875 radius=1.3029574931420822 gravity=129 pressure=1600 tempK=82 oxygen=false locked=false rings=false rotation=27854 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1889446_813181_4205893 1889421_813182_4205913 type=gasgiant mass=65.38391031628528 radius=5.529884538397504 gravity=214 pressure=1600 tempK=150 oxygen=false locked=false rings=false rotation=5200 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1889446_813181_4205893 1889432_813181_4205891 type=ice mass=0.003196048894076365 radius=0.2173409056280414 gravity=7 pressure=0 tempK=93 oxygen=false locked=false rings=false rotation=38068 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1889446_813181_4205893 1889438_813181_4205897 type=gasgiant mass=98.8886335867683 radius=6.619662381279651 gravity=226 pressure=1600 tempK=289 oxygen=false locked=false rings=true rotation=9797 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1889446_813181_4205893 1889441_813181_4205894 type=barren mass=0.021555134264941003 radius=0.3655036429954987 gravity=16 pressure=0 tempK=200 oxygen=false locked=true rings=false rotation=60504 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1889446_813181_4205893 1889442_813181_4205893 type=barren mass=0.03344889295789856 radius=0.3971078292923061 gravity=21 pressure=0 tempK=213 oxygen=false locked=true rings=false rotation=16301 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1889446_813181_4205893 1889445_813181_4205892 type=superearth mass=4.630557107800478 radius=1.458171431777551 gravity=218 pressure=1600 tempK=805 oxygen=false locked=true rings=false rotation=18664 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1889446_813181_4205893 1889446_813181_4205893 type=lava mass=12.173618416473394 radius=1.8776587580181643 gravity=345 pressure=669 tempK=1825 oxygen=false locked=true rings=false rotation=83253 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 1889446_813181_4205893 1889549_813175_4206001 type=ice mass=0.25307854023691273 radius=0.7244333784168657 gravity=48 pressure=725 tempK=53 oxygen=false locked=false rings=false rotation=95137 metallicity=0.3510160912922532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2198808_-575566_6365497 2198808_-575566_6365497 type=ice mass=10.702316197276062 radius=1.905262701085582 gravity=295 pressure=0 tempK=46 oxygen=false locked=false rings=false rotation=30986 metallicity=1.5889836291628834 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2233013_199342_2539057 2232959_199344_2538990 type=icegiant mass=203.06220854297288 radius=9.05102774503219 gravity=248 pressure=1600 tempK=77 oxygen=false locked=false rings=true rotation=7656 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2233013_199342_2539057 2233004_199342_2539063 type=exotic mass=2.037837169566401 radius=1.1851610699153496 gravity=145 pressure=1600 tempK=240 oxygen=false locked=false rings=false rotation=8889 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2233013_199342_2539057 2233011_199342_2539058 type=greenhouse mass=27.079448136395275 radius=2.4364227035505546 gravity=400 pressure=1600 tempK=468 oxygen=false locked=true rings=false rotation=7707 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2233013_199342_2539057 2233013_199342_2539057 type=lava mass=21.45480203020839 radius=2.380824182005102 gravity=379 pressure=869 tempK=1660 oxygen=false locked=true rings=false rotation=12623 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2233013_199342_2539057 2233058_199344_2539027 type=barren mass=0.009069448340871516 radius=0.2667562435907831 gravity=13 pressure=2 tempK=50 oxygen=false locked=false rings=false rotation=27998 metallicity=1.3783362879415066 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 2467378_-3394549_-3171449 2467378_-3394549_-3171449 type=ice mass=21.53973837804307 radius=2.356867373565178 gravity=388 pressure=0 tempK=49 oxygen=false locked=false rings=false rotation=20367 metallicity=1.0632904605963984 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3890455_2004932_-233592 3889392_2004932_-229119 type=ice mass=5.99660384759894 radius=1.5923441601989268 gravity=237 pressure=1600 tempK=26 oxygen=false locked=false rings=false rotation=12470 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3890455_2004932_-233592 3890445_2004933_-233643 type=barren mass=0.005337119337607637 radius=0.23580782176504658 gravity=10 pressure=0 tempK=63 oxygen=false locked=false rings=false rotation=46483 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3890455_2004932_-233592 3890454_2004932_-233594 type=greenhouse mass=2.334929894427855 radius=1.2566710178770715 gravity=148 pressure=654 tempK=462 oxygen=false locked=true rings=false rotation=11396 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3890455_2004932_-233592 3890455_2004932_-233592 type=lava mass=9.183708728610611 radius=1.955186935911926 gravity=240 pressure=375 tempK=1661 oxygen=false locked=true rings=false rotation=16376 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3890455_2004932_-233592 3890463_2004931_-233509 type=gasgiant mass=188.91827342424764 radius=8.771325497937614 gravity=246 pressure=1600 tempK=97 oxygen=false locked=false rings=true rotation=8516 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3890455_2004932_-233592 3890469_2004932_-233599 type=superearth mass=26.12871745362035 radius=2.2796306052080975 gravity=400 pressure=1600 tempK=246 oxygen=false locked=false rings=false rotation=10670 metallicity=1.5711933258486148 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 3996531_-3038111_5303430 3996531_-3038111_5303430 type=superearth mass=9.59325573931079 radius=1.9119707660338916 gravity=262 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=83725 metallicity=0.913772767522187 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 4299007_3832131_-2532498 4299007_3832131_-2532498 type=superearth mass=10.196513676437554 radius=1.8899347763673122 gravity=285 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=18997 metallicity=0.35588764546485174 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5022771_6570089_4140118 5022771_6570089_4140118 type=superearth mass=16.822013173824324 radius=2.2101822535923454 gravity=344 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=38517 metallicity=0.6020116330049018 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5089392_-2456012_2414774 5089392_-2456012_2414774 type=ice mass=0.31367799647917355 radius=0.7837832198512151 gravity=51 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=46696 metallicity=1.0065408351864933 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5718025_-888233_-3341159 5718020_-888233_-3341168 type=gasgiant mass=272.7082283176682 radius=10.289164946349796 gravity=258 pressure=1600 tempK=263 oxygen=false locked=false rings=true rotation=7327 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5718025_-888233_-3341159 5718023_-888233_-3341159 type=gasgiant mass=140.7497379219881 radius=7.717711568622234 gravity=236 pressure=1600 tempK=506 oxygen=false locked=false rings=true rotation=13821 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5718025_-888233_-3341159 5718024_-888233_-3341159 type=greenhouse mass=2.619618325045008 radius=1.2580658048627154 gravity=166 pressure=475 tempK=425 oxygen=false locked=true rings=false rotation=29644 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5718025_-888233_-3341159 5718025_-888233_-3341159 type=barren mass=0.2015654551402771 radius=0.6403317551321865 gravity=49 pressure=0 tempK=925 oxygen=false locked=true rings=false rotation=6285 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5718025_-888233_-3341159 5718075_-888232_-3341159 type=ice mass=0.010498482141557965 radius=0.2814384436049694 gravity=13 pressure=1 tempK=54 oxygen=false locked=false rings=false rotation=58420 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5718025_-888233_-3341159 5718083_-888231_-3341214 type=ice mass=0.8650462181584037 radius=0.9273899661488603 gravity=101 pressure=1600 tempK=97 oxygen=false locked=false rings=false rotation=8465 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5718025_-888233_-3341159 5718496_-888233_-3340282 type=icegiant mass=60.34971765958708 radius=5.34056888417804 gravity=212 pressure=1600 tempK=30 oxygen=false locked=false rings=false rotation=7297 metallicity=1.0317772741811058 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 5730187_2991355_6930094 5730187_2991355_6930094 type=barren mass=0.026437656144149838 radius=0.3614012444688947 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=14698 metallicity=1.094781397207314 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6244948_3150169_3269973 6244948_3150169_3269973 type=ice mass=0.5941041264344065 radius=0.9233026667448736 gravity=70 pressure=0 tempK=32 oxygen=false locked=false rings=false rotation=11951 metallicity=0.8666570370881141 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 6537086_5282959_646441 6537086_5282959_646441 type=barren mass=0.7284843849200809 radius=0.9432117495314174 gravity=82 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=17555 metallicity=0.4763162267116338 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 713381_4456830_2132934 713373_4456830_2132927 type=exotic mass=1.3455430172231895 radius=1.0653947740642289 gravity=119 pressure=1600 tempK=266 oxygen=false locked=false rings=false rotation=45031 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 713381_4456830_2132934 713374_4456833_2133007 type=ice mass=9.13046876726154 radius=1.8276728599659229 gravity=273 pressure=1600 tempK=86 oxygen=false locked=false rings=false rotation=15006 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 713381_4456830_2132934 713381_4456830_2132934 type=lava mass=5.398462527781383 radius=1.6840346511279936 gravity=190 pressure=90 tempK=1024 oxygen=false locked=true rings=false rotation=78615 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 713381_4456830_2132934 713383_4456830_2132933 type=barren mass=0.002488821616803771 radius=0.20757672260460924 gravity=6 pressure=0 tempK=257 oxygen=false locked=true rings=false rotation=41839 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 713381_4456830_2132934 713393_4456829_2132978 type=barren mass=0.048572146513674155 radius=0.4310713080560144 gravity=26 pressure=18 tempK=59 oxygen=false locked=false rings=false rotation=32112 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 713381_4456830_2132934 714948_4456830_2142291 type=ice mass=2.7218332020468132 radius=1.3317114481371697 gravity=153 pressure=1600 tempK=8 oxygen=false locked=false rings=false rotation=18149 metallicity=1.458080599653532 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 816822_6851185_5059304 814131_6851104_5058681 type=ice mass=0.18633243327335225 radius=0.6273201520127174 gravity=47 pressure=441 tempK=57 oxygen=false locked=false rings=false rotation=6608 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 816822_6851185_5059304 815136_6851223_5058930 type=ice mass=3.8710065602978716 radius=1.5437826210985488 gravity=162 pressure=1600 tempK=100 oxygen=false locked=false rings=false rotation=17376 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 816822_6851185_5059304 816691_6851184_5059714 type=gasgiant mass=69.69456445436751 radius=5.685540056018066 gravity=216 pressure=1600 tempK=212 oxygen=false locked=false rings=false rotation=7071 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 816822_6851185_5059304 816711_6851188_5059282 type=superearth mass=6.626827297311496 radius=1.7606471887988784 gravity=214 pressure=1600 tempK=449 oxygen=false locked=false rings=false rotation=44015 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 816822_6851185_5059304 816817_6851195_5059543 type=superearth mass=17.067554987748622 radius=2.172654173678009 gravity=362 pressure=1600 tempK=309 oxygen=false locked=false rings=false rotation=89074 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 816822_6851185_5059304 816818_6851185_5059302 type=lava mass=2.1018842181699178 radius=1.2823508765013565 gravity=128 pressure=32 tempK=1047 oxygen=false locked=true rings=false rotation=72051 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 816822_6851185_5059304 816822_6851185_5059304 type=lava mass=3.4209093327392703 radius=1.3205047953422844 gravity=196 pressure=1 tempK=5237 oxygen=false locked=true rings=false rotation=45668 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] - derived 816822_6851185_5059304 816843_6851183_5059334 type=barren mass=0.04617258486949676 radius=0.4210403808014127 gravity=26 pressure=0 tempK=369 oxygen=false locked=false rings=false rotation=29583 metallicity=0.35622707252048535 terrain=TerrainOption[NATIVE genType=0 w=1] - system -1396847_-3424625_-1950936 id=-207345417 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless - system -166709_6365358_-3072784 id=-1917888101 kind=STAR name=PGS--3525313.3525313.-3525313 starTemp=40 starSize=0.7003536820411682 - system -1798706_5881269_282340 id=-1883424565 kind=STAR name=PGS--3525313.3525313.0 starTemp=40 starSize=0.9460086822509766 - system -1842127_-2662654_258460 id=-1529010057 kind=ROGUE_PLANET name=PGR--3525313.-3525313.0 starless - system -2750569_1404918_6927999 id=-455553521 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless - system -2794652_-893855_4236484 id=-1572202913 kind=STAR name=PGS--3525313.-3525313.3525313 starTemp=40 starSize=0.747058093547821 - system -2976937_6586333_3889163 id=-1838315877 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless - system -3311706_3302146_-2970565 id=-36122729 kind=STAR name=PGS--3525313.0.-3525313 starTemp=40 starSize=0.7600389122962952 - system -671411_3391485_519485 id=-1864177861 kind=ROGUE_PLANET name=PGR--3525313.0.0 starless - system 1594627_-205296_2117674 id=-1076464245 kind=STAR name=PGS-0.-3525313.0 starTemp=40 starSize=0.6730666756629944 - system 1763248_3924110_-951397 id=-965124545 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless - system 1859067_623717_-2088070 id=-1701373473 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless - system 1889446_813181_4205893 id=-1019483873 kind=STAR name=PGS-0.0.3525313 starTemp=40 starSize=0.8574948906898499 - system 2198808_-575566_6365497 id=-1785782589 kind=ROGUE_PLANET name=PGR-0.-3525313.3525313 starless - system 2233013_199342_2539057 id=-1579160837 kind=STAR name=PGS-0.0.0 starTemp=40 starSize=0.6222827434539795 - system 2467378_-3394549_-3171449 id=-1420637497 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless - system 3890455_2004932_-233592 id=-1175248789 kind=STAR name=PGS-3525313.0.-3525313 starTemp=40 starSize=0.94906085729599 - system 3996531_-3038111_5303430 id=-689584929 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless - system 4299007_3832131_-2532498 id=-657806589 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless - system 5022771_6570089_4140118 id=-1402022073 kind=ROGUE_PLANET name=PGR-3525313.3525313.3525313 starless - system 5089392_-2456012_2414774 id=-474729317 kind=ROGUE_PLANET name=PGR-3525313.-3525313.0 starless - system 5718025_-888233_-3341159 id=-1451948173 kind=STAR name=PGS-3525313.-3525313.-3525313 starTemp=40 starSize=0.7285904288291931 - system 5730187_2991355_6930094 id=-940407273 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless - system 6244948_3150169_3269973 id=-1552747849 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless - system 6537086_5282959_646441 id=-715739421 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless - system 713381_4456830_2132934 id=-373901957 kind=STAR name=PGS-0.3525313.0 starTemp=40 starSize=0.7361619472503662 - system 816822_6851185_5059304 id=-547337629 kind=STAR name=PGS-0.3525313.3525313 starTemp=150 starSize=1.6428048610687256 +seed 1 systems=10 + body -2557100_423979_3637445 -2557100_423979_3637445 kind=MOON orbit=0 radius=1.8447368918446045 starId=-1134337153 frame=false at=-76183,0,-351233 + body -2557100_423979_3637445 -2557100_423979_3637445 kind=MOON orbit=0 radius=2.414101865417722 starId=-1134337153 frame=false at=-259103,0,-152795 + body -2557100_423979_3637445 -2557100_423979_3637445 kind=ROGUE_PLANET orbit=0 radius=1.4814447373908066 starId=-1134337153 frame=true at=0,0,0 + body -2655791_3860139_4556832 -2655791_3860139_4556832 kind=ROGUE_PLANET orbit=0 radius=1.9480617919050078 starId=-157245369 frame=true at=0,0,0 + body -2673416_-3299924_-2542369 -2673416_-3299924_-2542369 kind=ROGUE_PLANET orbit=0 radius=0.5140134458397283 starId=-457888649 frame=true at=0,0,0 + body -3057060_4504688_903368 -3057060_4504688_903368 kind=MOON orbit=0 radius=0.6738095421217645 starId=-392700849 frame=false at=-216285,0,98841 + body -3057060_4504688_903368 -3057060_4504688_903368 kind=MOON orbit=0 radius=1.9863890836269211 starId=-392700849 frame=false at=86332,0,76878 + body -3057060_4504688_903368 -3057060_4504688_903368 kind=ROGUE_PLANET orbit=0 radius=0.9265150238967861 starId=-392700849 frame=true at=0,0,0 + body 1017499_549209_-2760817 1017499_549209_-2760817 kind=MOON orbit=0 radius=1.2312835780207372 starId=-268045669 frame=false at=211577,0,124718 + body 1017499_549209_-2760817 1017499_549209_-2760817 kind=ROGUE_PLANET orbit=0 radius=1.6175224369918324 starId=-268045669 frame=true at=0,0,0 + body 198207_-2510563_-2496053 198207_-2510563_-2496053 kind=ROGUE_PLANET orbit=0 radius=1.1591832445693329 starId=-914263305 frame=true at=0,0,0 + body 3639726_-2990703_-2920715 3639726_-2990703_-2920715 kind=MOON orbit=0 radius=1.5678731162225399 starId=-640125417 frame=false at=-160108,0,-85272 + body 3639726_-2990703_-2920715 3639726_-2990703_-2920715 kind=ROGUE_PLANET orbit=0 radius=1.1642707970044215 starId=-640125417 frame=true at=0,0,0 + body 3823355_3745237_320026 3823355_3745237_320026 kind=ROGUE_PLANET orbit=0 radius=1.2113628889117705 starId=-1337270441 frame=true at=0,0,0 + body 4187637_602139_-2869399 4187637_602139_-2869399 kind=ROGUE_PLANET orbit=0 radius=1.7454789147745489 starId=-636290765 frame=true at=0,0,0 + body 976378_3636775_4086471 976378_3636775_4086471 kind=ROGUE_PLANET orbit=0 radius=0.6852144335756171 starId=-1484690657 frame=true at=0,0,0 + derived -2557100_423979_3637445 -2557100_423979_3637445 type=superearth mass=4.944099558821902 radius=1.4814447373908066 gravity=225 pressure=0 tempK=43 oxygen=false locked=false rings=false rotation=14161 metallicity=0.7675045838421682 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2655791_3860139_4556832 -2655791_3860139_4556832 type=ice mass=8.953986377182925 radius=1.9480617919050078 gravity=236 pressure=0 tempK=43 oxygen=false locked=false rings=false rotation=14966 metallicity=1.023458230313111 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2673416_-3299924_-2542369 -2673416_-3299924_-2542369 type=barren mass=0.08742513278618706 radius=0.5140134458397283 gravity=33 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=37319 metallicity=0.5432850188533043 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3057060_4504688_903368 -3057060_4504688_903368 type=ice mass=0.6907407385519565 radius=0.9265150238967861 gravity=80 pressure=0 tempK=33 oxygen=false locked=false rings=false rotation=24780 metallicity=1.4771347200268488 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1017499_549209_-2760817 1017499_549209_-2760817 type=superearth mass=6.720000591465987 radius=1.6175224369918324 gravity=257 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=25973 metallicity=1.5348057666632275 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 198207_-2510563_-2496053 198207_-2510563_-2496053 type=ice mass=1.8782374156749306 radius=1.1591832445693329 gravity=140 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=81388 metallicity=1.5252855058290513 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3639726_-2990703_-2920715 3639726_-2990703_-2920715 type=ice mass=1.4281056559244625 radius=1.1642707970044215 gravity=105 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=30927 metallicity=0.43414167394403486 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3823355_3745237_320026 3823355_3745237_320026 type=ice mass=2.3245028760180175 radius=1.2113628889117705 gravity=158 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=34397 metallicity=1.4633582267369367 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4187637_602139_-2869399 4187637_602139_-2869399 type=ice mass=8.033586448333969 radius=1.7454789147745489 gravity=264 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=12742 metallicity=0.8463677000016181 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 976378_3636775_4086471 976378_3636775_4086471 type=ice mass=0.23914383202211043 radius=0.6852144335756171 gravity=51 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=25305 metallicity=0.4173546565671391 terrain=TerrainOption[NATIVE genType=0 w=1] + system -2557100_423979_3637445 id=-1134337153 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless + system -2655791_3860139_4556832 id=-157245369 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system -2673416_-3299924_-2542369 id=-457888649 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless + system -3057060_4504688_903368 id=-392700849 kind=ROGUE_PLANET name=PGR--3525313.3525313.0 starless + system 1017499_549209_-2760817 id=-268045669 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless + system 198207_-2510563_-2496053 id=-914263305 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless + system 3639726_-2990703_-2920715 id=-640125417 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless + system 3823355_3745237_320026 id=-1337270441 kind=ROGUE_PLANET name=PGR-3525313.3525313.0 starless + system 4187637_602139_-2869399 id=-636290765 kind=ROGUE_PLANET name=PGR-3525313.0.-3525313 starless + system 976378_3636775_4086471 id=-1484690657 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless +seed 42 systems=5 + body -2447011_3990067_1015768 -2447011_3990067_1015768 kind=ROGUE_PLANET orbit=0 radius=1.989945249401805 starId=-303078433 frame=true at=0,0,0 + body -3084373_3721958_-2836759 -3084373_3721958_-2836759 kind=MOON orbit=0 radius=1.2391946808283263 starId=-179570697 frame=false at=150517,0,136807 + body -3084373_3721958_-2836759 -3084373_3721958_-2836759 kind=ROGUE_PLANET orbit=0 radius=0.6685266599064654 starId=-179570697 frame=true at=0,0,0 + body 1035243_295080_-2679253 1035243_295080_-2679253 kind=ROGUE_PLANET orbit=0 radius=1.3473679621970385 starId=-1652144877 frame=true at=0,0,0 + body 190640_4091718_-3216585 190640_4091718_-3216585 kind=ROGUE_PLANET orbit=0 radius=1.538054696275251 starId=-502853333 frame=true at=0,0,0 + body 3842895_743694_4157756 3842895_743694_4157756 kind=MOON orbit=0 radius=0.6284105220560577 starId=-1165534741 frame=false at=-112346,0,-34758 + body 3842895_743694_4157756 3842895_743694_4157756 kind=ROGUE_PLANET orbit=0 radius=0.5198799197186048 starId=-1165534741 frame=true at=0,0,0 + derived -2447011_3990067_1015768 -2447011_3990067_1015768 type=superearth mass=10.088778310024445 radius=1.989945249401805 gravity=255 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=79920 metallicity=1.5777697067277474 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3084373_3721958_-2836759 -3084373_3721958_-2836759 type=barren mass=0.24881424393731585 radius=0.6685266599064654 gravity=56 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=47156 metallicity=0.871880601372064 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1035243_295080_-2679253 1035243_295080_-2679253 type=ice mass=2.4573812328128533 radius=1.3473679621970385 gravity=135 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=40099 metallicity=1.5785510692143445 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 190640_4091718_-3216585 190640_4091718_-3216585 type=superearth mass=4.8180484411980276 radius=1.538054696275251 gravity=204 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=19264 metallicity=0.7186063878043278 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3842895_743694_4157756 3842895_743694_4157756 type=barren mass=0.08318382388923339 radius=0.5198799197186048 gravity=31 pressure=0 tempK=26 oxygen=false locked=false rings=false rotation=70969 metallicity=0.6776328297933261 terrain=TerrainOption[NATIVE genType=0 w=1] + system -2447011_3990067_1015768 id=-303078433 kind=ROGUE_PLANET name=PGR--3525313.3525313.0 starless + system -3084373_3721958_-2836759 id=-179570697 kind=ROGUE_PLANET name=PGR--3525313.3525313.-3525313 starless + system 1035243_295080_-2679253 id=-1652144877 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless + system 190640_4091718_-3216585 id=-502853333 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless + system 3842895_743694_4157756 id=-1165534741 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless +seed 1337 systems=3 + body 276785_671277_3636639 276785_671277_3636639 kind=MOON orbit=0 radius=0.2956238533945368 starId=-1715483833 frame=false at=-372883,0,19642 + body 276785_671277_3636639 276785_671277_3636639 kind=MOON orbit=0 radius=0.30796799570160793 starId=-1715483833 frame=false at=175063,0,31076 + body 276785_671277_3636639 276785_671277_3636639 kind=ROGUE_PLANET orbit=0 radius=1.3493157557301982 starId=-1715483833 frame=true at=0,0,0 + body 4120657_566754_604545 4120657_566754_604545 kind=ROGUE_PLANET orbit=0 radius=0.31963922617475793 starId=-847248597 frame=true at=0,0,0 + body 4597503_3784885_-3111897 4597503_3784885_-3111897 kind=ROGUE_PLANET orbit=0 radius=1.0444056015522982 starId=-1405460665 frame=true at=0,0,0 + derived 276785_671277_3636639 276785_671277_3636639 type=superearth mass=3.437098370037941 radius=1.3493157557301982 gravity=189 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=15830 metallicity=0.7860854234824994 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4120657_566754_604545 4120657_566754_604545 type=barren mass=0.015365480193628647 radius=0.31963922617475793 gravity=15 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=32523 metallicity=0.7510293998351121 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4597503_3784885_-3111897 4597503_3784885_-3111897 type=ice mass=1.0972317554809465 radius=1.0444056015522982 gravity=101 pressure=0 tempK=35 oxygen=false locked=false rings=false rotation=36365 metallicity=1.0545794786678782 terrain=TerrainOption[NATIVE genType=0 w=1] + system 276785_671277_3636639 id=-1715483833 kind=ROGUE_PLANET name=PGR-0.0.3525313 starless + system 4120657_566754_604545 id=-847248597 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless + system 4597503_3784885_-3111897 id=-1405460665 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless +seed 8675309 systems=2 + body 1046546_-2903040_725654 1046546_-2903040_725654 kind=ROGUE_PLANET orbit=0 radius=0.6470070511607549 starId=-1628626657 frame=true at=0,0,0 + body 1063728_-2564249_-3016086 1063728_-2564249_-3016086 kind=ROGUE_PLANET orbit=0 radius=1.935067436969281 starId=-193170121 frame=true at=0,0,0 + derived 1046546_-2903040_725654 1046546_-2903040_725654 type=barren mass=0.15646924852335253 radius=0.6470070511607549 gravity=37 pressure=0 tempK=27 oxygen=false locked=false rings=false rotation=27503 metallicity=0.9774911484366974 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 1063728_-2564249_-3016086 1063728_-2564249_-3016086 type=ice mass=9.74459618037882 radius=1.935067436969281 gravity=260 pressure=0 tempK=44 oxygen=false locked=false rings=false rotation=7049 metallicity=1.075249972161227 terrain=TerrainOption[NATIVE genType=0 w=1] + system 1046546_-2903040_725654 id=-1628626657 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless + system 1063728_-2564249_-3016086 id=-193170121 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless +seed -1 systems=8 + body -2782949_-3335311_-3172048 -2782949_-3335311_-3172048 kind=MOON orbit=0 radius=0.3811324069014488 starId=-304935909 frame=false at=-222003,0,-267730 + body -2782949_-3335311_-3172048 -2782949_-3335311_-3172048 kind=MOON orbit=0 radius=1.7827041269401591 starId=-304935909 frame=false at=-186393,0,-19400 + body -2782949_-3335311_-3172048 -2782949_-3335311_-3172048 kind=ROGUE_PLANET orbit=0 radius=1.2158739257305007 starId=-304935909 frame=true at=0,0,0 + body -3287314_3807297_3941148 -3287314_3807297_3941148 kind=ROGUE_PLANET orbit=0 radius=0.33955082250013896 starId=-1160817649 frame=true at=0,0,0 + body 3664834_3776565_4532932 3664834_3776565_4532932 kind=MOON orbit=0 radius=0.5519257469023342 starId=-284738901 frame=false at=56749,0,-135821 + body 3664834_3776565_4532932 3664834_3776565_4532932 kind=MOON orbit=0 radius=2.3292049800084142 starId=-284738901 frame=false at=79151,0,113533 + body 3664834_3776565_4532932 3664834_3776565_4532932 kind=ROGUE_PLANET orbit=0 radius=0.6082699126970779 starId=-284738901 frame=true at=0,0,0 + body 3853702_543117_1039757 3853702_543117_1039757 kind=ROGUE_PLANET orbit=0 radius=0.9898277293392106 starId=-589964249 frame=true at=0,0,0 + body 4483478_-2850483_3653003 4483478_-2850483_3653003 kind=MOON orbit=0 radius=1.7411223799459588 starId=-1321358033 frame=false at=382344,0,106874 + body 4483478_-2850483_3653003 4483478_-2850483_3653003 kind=ROGUE_PLANET orbit=0 radius=1.435001513340223 starId=-1321358033 frame=true at=0,0,0 + body 743192_-3039588_157466 743192_-3039588_157466 kind=MOON orbit=0 radius=1.8375370381187128 starId=-1078102009 frame=false at=-34099,0,10903 + body 743192_-3039588_157466 743192_-3039588_157466 kind=ROGUE_PLANET orbit=0 radius=0.4112854086051495 starId=-1078102009 frame=true at=0,0,0 + body 878581_-3017002_4553300 872959_-3017002_4556800 kind=STAR orbit=35417 radius=75.04819331288338 starId=-1897301650 frame=true at=0,0,0 + body 878581_-3017002_4553300 878553_-3017004_4553343 kind=PLANET orbit=276 radius=1.5157966496043178 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878566_-3017002_4553315 kind=MOON orbit=116 radius=0.20652907168649953 starId=-1897301649 frame=false at=339405,0,282827 + body 878581_-3017002_4553300 878566_-3017002_4553315 kind=MOON orbit=116 radius=0.5437577907459678 starId=-1897301649 frame=false at=511462,0,-30996 + body 878581_-3017002_4553300 878566_-3017002_4553315 kind=PLANET orbit=116 radius=1.9962282827789826 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878572_-3017003_4553306 kind=PLANET orbit=59 radius=0.3484083052264197 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878575_-3017002_4553288 kind=GAS_GIANT orbit=70 radius=9.106319267554383 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878578_-3017002_4553300 kind=ASTEROID_BELT orbit=15 radius=0.0 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878580_-3017002_4553299 kind=PLANET orbit=9 radius=0.7705187789339893 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878580_-3017002_4553302 kind=PLANET orbit=15 radius=0.7882028558055201 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878581_-3017002_4553299 kind=MOON orbit=6 radius=0.3285151555828062 starId=-1897301649 frame=false at=-7978,0,19857 + body 878581_-3017002_4553300 878581_-3017002_4553299 kind=PLANET orbit=6 radius=0.21883050688087927 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878581_-3017002_4553300 kind=STAR orbit=0 radius=0.0 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878584_-3017002_4553302 kind=MOON orbit=19 radius=0.2091041602077615 starId=-1897301649 frame=false at=-57606,0,22378 + body 878581_-3017002_4553300 878584_-3017002_4553302 kind=PLANET orbit=19 radius=0.6020719857479297 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878586_-3017002_4553298 kind=GAS_GIANT orbit=27 radius=4.402691577407306 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878588_-3017002_4553300 kind=MOON orbit=39 radius=0.3351591521971312 starId=-1897301649 frame=false at=93945,0,15410 + body 878581_-3017002_4553300 878588_-3017002_4553300 kind=MOON orbit=39 radius=0.6094488945358825 starId=-1897301649 frame=false at=62749,0,-33221 + body 878581_-3017002_4553300 878588_-3017002_4553300 kind=PLANET orbit=39 radius=0.3946638194579618 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878597_-3017003_4553325 kind=GAS_GIANT orbit=160 radius=6.614858175508893 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878597_-3017003_4553325 kind=MOON orbit=160 radius=0.6525910316854073 starId=-1897301649 frame=false at=-1396614,0,1139032 + body 878581_-3017002_4553300 878612_-3017003_4553330 kind=GAS_GIANT orbit=230 radius=3.2383609634026165 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878644_-3017001_4553348 kind=MOON orbit=424 radius=0.23115588832734993 starId=-1897301649 frame=false at=-57990,0,-77757 + body 878581_-3017002_4553300 878644_-3017001_4553348 kind=PLANET orbit=424 radius=0.3567889137917456 starId=-1897301649 frame=true at=0,0,0 + body 878581_-3017002_4553300 878680_-3017002_4553221 kind=ASTEROID_BELT orbit=678 radius=0.0 starId=-1897301649 frame=true at=0,0,0 + body 905332_3909756_4056715 905332_3909756_4056715 kind=ROGUE_PLANET orbit=0 radius=1.6232022956018095 starId=-1020620017 frame=true at=0,0,0 + derived -2782949_-3335311_-3172048 -2782949_-3335311_-3172048 type=ice mass=2.1148586853111633 radius=1.2158739257305007 gravity=143 pressure=0 tempK=38 oxygen=false locked=false rings=false rotation=20246 metallicity=0.38296507979539846 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -3287314_3807297_3941148 -3287314_3807297_3941148 type=barren mass=0.022449370251015673 radius=0.33955082250013896 gravity=19 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=10256 metallicity=0.5725117772717905 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3664834_3776565_4532932 3664834_3776565_4532932 type=barren mass=0.1517947540551749 radius=0.6082699126970779 gravity=41 pressure=0 tempK=28 oxygen=false locked=false rings=false rotation=6065 metallicity=0.5186924118210909 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3853702_543117_1039757 3853702_543117_1039757 type=barren mass=0.8458563337067997 radius=0.9898277293392106 gravity=86 pressure=0 tempK=34 oxygen=false locked=false rings=false rotation=18539 metallicity=1.0208698050614735 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4483478_-2850483_3653003 4483478_-2850483_3653003 type=ice mass=3.0464390892959123 radius=1.435001513340223 gravity=148 pressure=0 tempK=39 oxygen=false locked=false rings=false rotation=13841 metallicity=1.3324770895609348 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 743192_-3039588_157466 743192_-3039588_157466 type=barren mass=0.033909515805476215 radius=0.4112854086051495 gravity=20 pressure=0 tempK=23 oxygen=false locked=false rings=false rotation=26070 metallicity=1.586073172708923 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 872959_-3017002_4556800 type=superearth mass=3.75245081793383 radius=1.3770899672024957 gravity=198 pressure=1600 tempK=11 oxygen=false locked=false rings=false rotation=19857 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878553_-3017004_4553343 type=ice mass=5.754990119461963 radius=1.5157966496043178 gravity=250 pressure=1600 tempK=101 oxygen=false locked=false rings=false rotation=79184 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878566_-3017002_4553315 type=ice mass=9.764724238485714 radius=1.9962282827789826 gravity=245 pressure=1600 tempK=156 oxygen=false locked=false rings=false rotation=70365 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878572_-3017003_4553306 type=ice mass=0.016668815448320475 radius=0.3484083052264197 gravity=14 pressure=1 tempK=97 oxygen=false locked=false rings=false rotation=17983 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878575_-3017002_4553288 type=icegiant mass=205.92664840109282 radius=9.106319267554383 gravity=248 pressure=1600 tempK=213 oxygen=false locked=false rings=true rotation=9791 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878578_-3017002_4553300 type=desert mass=0.10615272231334011 radius=0.5866299417116883 gravity=31 pressure=3 tempK=222 oxygen=false locked=true rings=false rotation=73625 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878580_-3017002_4553299 type=barren mass=0.3390944455898112 radius=0.7705187789339893 gravity=57 pressure=13 tempK=304 oxygen=false locked=true rings=false rotation=83680 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878580_-3017002_4553302 type=ice mass=0.3704474492415404 radius=0.7882028558055201 gravity=60 pressure=30 tempK=193 oxygen=false locked=true rings=false rotation=85552 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878581_-3017002_4553299 type=barren mass=0.004279089534781598 radius=0.21883050688087927 gravity=9 pressure=0 tempK=372 oxygen=false locked=true rings=false rotation=50818 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878581_-3017002_4553300 type=barren mass=0.003697376618150787 radius=0.2099118806935753 gravity=8 pressure=0 tempK=912 oxygen=false locked=true rings=false rotation=50445 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878584_-3017002_4553302 type=ice mass=0.1570807560599777 radius=0.6020719857479297 gravity=43 pressure=18 tempK=171 oxygen=false locked=true rings=false rotation=74322 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878586_-3017002_4553298 type=gasgiant mass=38.705791121690105 radius=4.402691577407306 gravity=200 pressure=1600 tempK=343 oxygen=false locked=false rings=true rotation=12478 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878588_-3017002_4553300 type=barren mass=0.03288029815145879 radius=0.3946638194579618 gravity=21 pressure=1 tempK=146 oxygen=false locked=true rings=false rotation=25710 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878597_-3017003_4553325 type=gasgiant mass=98.72364455770969 radius=6.614858175508893 gravity=226 pressure=1600 tempK=140 oxygen=false locked=false rings=true rotation=7309 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878612_-3017003_4553330 type=gasgiant mass=19.09730271296885 radius=3.2383609634026165 gravity=182 pressure=1600 tempK=117 oxygen=false locked=false rings=false rotation=11030 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878644_-3017001_4553348 type=barren mass=0.026873364146008688 radius=0.3567889137917456 gravity=21 pressure=11 tempK=44 oxygen=false locked=false rings=false rotation=31938 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 878581_-3017002_4553300 878680_-3017002_4553221 type=superearth mass=24.824960233297325 radius=2.3661964095752266 gravity=400 pressure=1600 tempK=74 oxygen=false locked=false rings=false rotation=90978 metallicity=0.8668625908386669 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 905332_3909756_4056715 905332_3909756_4056715 type=superearth mass=7.049954839078062 radius=1.6232022956018095 gravity=268 pressure=0 tempK=45 oxygen=false locked=false rings=false rotation=9498 metallicity=0.7464841711558176 terrain=TerrainOption[NATIVE genType=0 w=1] + system -2782949_-3335311_-3172048 id=-304935909 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless + system -3287314_3807297_3941148 id=-1160817649 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system 3664834_3776565_4532932 id=-284738901 kind=ROGUE_PLANET name=PGR-3525313.3525313.3525313 starless + system 3853702_543117_1039757 id=-589964249 kind=ROGUE_PLANET name=PGR-3525313.0.0 starless + system 4483478_-2850483_3653003 id=-1321358033 kind=ROGUE_PLANET name=PGR-3525313.-3525313.3525313 starless + system 743192_-3039588_157466 id=-1078102009 kind=ROGUE_PLANET name=PGR-0.-3525313.0 starless + system 878581_-3017002_4553300 id=-1897301649 kind=STAR name=PGS-0.-3525313.3525313 starTemp=40 starSize=0.709514319896698 + system 905332_3909756_4056715 id=-1020620017 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless +seed 6942069 systems=4 + body -2734142_4401718_4309207 -2734142_4401718_4309207 kind=ROGUE_PLANET orbit=0 radius=0.43631553719843885 starId=-66168209 frame=true at=0,0,0 + body -2908371_-2556424_3786953 -2908371_-2556424_3786953 kind=ROGUE_PLANET orbit=0 radius=1.908019290563573 starId=-264498101 frame=true at=0,0,0 + body 744853_3857094_4340709 744853_3857094_4340709 kind=MOON orbit=0 radius=0.31342515786159897 starId=-1566341957 frame=false at=2946252,0,117650 + body 744853_3857094_4340709 744853_3857094_4340709 kind=ROGUE_PLANET orbit=0 radius=10.804886830349613 starId=-1566341957 frame=true at=0,0,0 + body 748976_106008_-3408393 748976_106008_-3408393 kind=ROGUE_PLANET orbit=0 radius=2.171011837467088 starId=-333451093 frame=true at=0,0,0 + derived -2734142_4401718_4309207 -2734142_4401718_4309207 type=barren mass=0.0410412584139839 radius=0.43631553719843885 gravity=22 pressure=0 tempK=24 oxygen=false locked=false rings=false rotation=18269 metallicity=0.4876802064323611 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2908371_-2556424_3786953 -2908371_-2556424_3786953 type=superearth mass=12.982415793011713 radius=1.908019290563573 gravity=357 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=71846 metallicity=0.5583921546780021 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 744853_3857094_4340709 744853_3857094_4340709 type=icegiant mass=305.1760558389291 radius=10.804886830349613 gravity=261 pressure=1600 tempK=45 oxygen=false locked=false rings=true rotation=9642 metallicity=0.5678204702000748 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 748976_106008_-3408393 748976_106008_-3408393 type=ice mass=15.421880528808861 radius=2.171011837467088 gravity=327 pressure=0 tempK=47 oxygen=false locked=false rings=false rotation=39235 metallicity=0.3854381084175446 terrain=TerrainOption[NATIVE genType=0 w=1] + system -2734142_4401718_4309207 id=-66168209 kind=ROGUE_PLANET name=PGR--3525313.3525313.3525313 starless + system -2908371_-2556424_3786953 id=-264498101 kind=ROGUE_PLANET name=PGR--3525313.-3525313.3525313 starless + system 744853_3857094_4340709 id=-1566341957 kind=ROGUE_PLANET name=PGR-0.3525313.3525313 starless + system 748976_106008_-3408393 id=-333451093 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless +seed 2147483647 systems=9 + body -2493723_-2839512_-3212741 -2493723_-2839512_-3212741 kind=MOON orbit=0 radius=0.33837544643821515 starId=-207345417 frame=false at=-47705,0,-10277 + body -2493723_-2839512_-3212741 -2493723_-2839512_-3212741 kind=MOON orbit=0 radius=0.5074630002826466 starId=-207345417 frame=false at=-38969,0,5810 + body -2493723_-2839512_-3212741 -2493723_-2839512_-3212741 kind=ROGUE_PLANET orbit=0 radius=0.338498394763068 starId=-207345417 frame=true at=0,0,0 + body -2742838_660938_3979190 -2742838_660938_3979190 kind=ROGUE_PLANET orbit=0 radius=0.22137561323537092 starId=-455553521 frame=true at=0,0,0 + body 3834728_104915_3754932 3834728_104915_3754932 kind=MOON orbit=0 radius=2.2091575932064966 starId=-940407273 frame=false at=-26561,0,431984 + body 3834728_104915_3754932 3834728_104915_3754932 kind=ROGUE_PLANET orbit=0 radius=1.5925684061535028 starId=-940407273 frame=true at=0,0,0 + body 4037900_4371065_-3063872 4037900_4371065_-3063872 kind=ROGUE_PLANET orbit=0 radius=2.093359451025224 starId=-657806589 frame=true at=0,0,0 + body 4348408_-3060116_-3223142 4348408_-3060116_-3223142 kind=MOON orbit=0 radius=2.3843255919435293 starId=-1865164581 frame=false at=129914,0,87799 + body 4348408_-3060116_-3223142 4348408_-3060116_-3223142 kind=ROGUE_PLANET orbit=0 radius=1.7114977812892795 starId=-1865164581 frame=true at=0,0,0 + body 712998_-3013860_-3318391 712998_-3013860_-3318391 kind=MOON orbit=0 radius=2.095818752703184 starId=-1420637497 frame=false at=9595,0,81437 + body 712998_-3013860_-3318391 712998_-3013860_-3318391 kind=MOON orbit=0 radius=6.667876582430962 starId=-1420637497 frame=false at=87182,0,-11969 + body 712998_-3013860_-3318391 712998_-3013860_-3318391 kind=ROGUE_PLANET orbit=0 radius=0.5936672169388442 starId=-1420637497 frame=true at=0,0,0 + body 718851_3779944_691719 718851_3779944_691719 kind=MOON orbit=0 radius=1.5472070306152543 starId=-1442192965 frame=false at=100219,0,-18977 + body 718851_3779944_691719 718851_3779944_691719 kind=MOON orbit=0 radius=2.113264014264038 starId=-1442192965 frame=false at=64429,0,-28867 + body 718851_3779944_691719 718851_3779944_691719 kind=ROGUE_PLANET orbit=0 radius=0.3838652235975247 starId=-1442192965 frame=true at=0,0,0 + body 744632_4318880_-2812679 744632_4318880_-2812679 kind=MOON orbit=0 radius=4.676773652233241 starId=-965124545 frame=false at=116755,0,318473 + body 744632_4318880_-2812679 744632_4318880_-2812679 kind=ROGUE_PLANET orbit=0 radius=2.280305265104321 starId=-965124545 frame=true at=0,0,0 + body 974479_672038_-3200167 974479_672038_-3200167 kind=ROGUE_PLANET orbit=0 radius=0.2751076136224251 starId=-1701373473 frame=true at=0,0,0 + derived -2493723_-2839512_-3212741 -2493723_-2839512_-3212741 type=barren mass=0.014821975902813578 radius=0.338498394763068 gravity=13 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=63594 metallicity=0.404360462100807 terrain=TerrainOption[NATIVE genType=0 w=1] + derived -2742838_660938_3979190 -2742838_660938_3979190 type=ice mass=0.004697374280323576 radius=0.22137561323537092 gravity=10 pressure=0 tempK=19 oxygen=false locked=false rings=false rotation=22932 metallicity=0.7697090492061979 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 3834728_104915_3754932 3834728_104915_3754932 type=ice mass=5.40917540606095 radius=1.5925684061535028 gravity=213 pressure=0 tempK=42 oxygen=false locked=false rings=false rotation=37513 metallicity=0.6300762958523322 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4037900_4371065_-3063872 4037900_4371065_-3063872 type=ice mass=15.897400529235586 radius=2.093359451025224 gravity=363 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=14555 metallicity=0.4155887210628324 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 4348408_-3060116_-3223142 4348408_-3060116_-3223142 type=superearth mass=5.782581482149673 radius=1.7114977812892795 gravity=197 pressure=0 tempK=41 oxygen=false locked=false rings=false rotation=13613 metallicity=1.0113311064379111 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 712998_-3013860_-3318391 712998_-3013860_-3318391 type=ice mass=0.18065730676369665 radius=0.5936672169388442 gravity=51 pressure=0 tempK=30 oxygen=false locked=false rings=false rotation=57108 metallicity=0.37057061467784547 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 718851_3779944_691719 718851_3779944_691719 type=barren mass=0.023920205370259073 radius=0.3838652235975247 gravity=16 pressure=0 tempK=22 oxygen=false locked=false rings=false rotation=22790 metallicity=0.9000540929487959 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 744632_4318880_-2812679 744632_4318880_-2812679 type=ice mass=18.458429998067526 radius=2.280305265104321 gravity=355 pressure=0 tempK=48 oxygen=false locked=false rings=false rotation=20045 metallicity=0.8219291577052881 terrain=TerrainOption[NATIVE genType=0 w=1] + derived 974479_672038_-3200167 974479_672038_-3200167 type=barren mass=0.009298637248549268 radius=0.2751076136224251 gravity=12 pressure=0 tempK=21 oxygen=false locked=false rings=false rotation=30428 metallicity=1.5915795954966696 terrain=TerrainOption[NATIVE genType=0 w=1] + system -2493723_-2839512_-3212741 id=-207345417 kind=ROGUE_PLANET name=PGR--3525313.-3525313.-3525313 starless + system -2742838_660938_3979190 id=-455553521 kind=ROGUE_PLANET name=PGR--3525313.0.3525313 starless + system 3834728_104915_3754932 id=-940407273 kind=ROGUE_PLANET name=PGR-3525313.0.3525313 starless + system 4037900_4371065_-3063872 id=-657806589 kind=ROGUE_PLANET name=PGR-3525313.3525313.-3525313 starless + system 4348408_-3060116_-3223142 id=-1865164581 kind=ROGUE_PLANET name=PGR-3525313.-3525313.-3525313 starless + system 712998_-3013860_-3318391 id=-1420637497 kind=ROGUE_PLANET name=PGR-0.-3525313.-3525313 starless + system 718851_3779944_691719 id=-1442192965 kind=ROGUE_PLANET name=PGR-0.3525313.0 starless + system 744632_4318880_-2812679 id=-965124545 kind=ROGUE_PLANET name=PGR-0.3525313.-3525313 starless + system 974479_672038_-3200167 id=-1701373473 kind=ROGUE_PLANET name=PGR-0.0.-3525313 starless From d9fe1f9732b547570fdededa185efbfe795d4ebb Mon Sep 17 00:00:00 2001 From: StannisMod Date: Wed, 19 Aug 2026 21:20:50 +0300 Subject: [PATCH 41/42] feat: making out what orbits a star costs more light than finding it - resolution needs a margin of magnitudes over the detection limit - the margin is derived from the signal-to-noise two tasks need - resolvability decided where the aperture is known, carried on the hit - an unresolved system is still written down as a position --- .../advancedRocketry/api/ARConfiguration.java | 21 ++++ .../universe/TelescopeScan.java | 58 ++++++++- .../test/unit/TelescopeConeSurveyTest.java | 114 +++++++++++++++++- 3 files changed, 189 insertions(+), 4 deletions(-) diff --git a/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java b/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java index f3408668f..d7ced1d88 100644 --- a/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java +++ b/src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java @@ -298,6 +298,24 @@ public class ARConfiguration { public static final double DEFAULT_TELESCOPE_LIMITING_MAGNITUDE = 8d; /** @see #DEFAULT_TELESCOPE_LIMITING_MAGNITUDE */ public static final double DEFAULT_TELESCOPE_CONE_HALF_ANGLE_DEGREES = 1d; + /** + * How much BRIGHTER than the detection limit a system must be before an instrument can make out + * what is in it — 6.5 magnitudes, and the number is derived rather than chosen. + * + *

    Seeing that a point of light is there and measuring what orbits it are not the same + * observation. Detection is conventionally called at a signal-to-noise of about 5 — enough to + * say "something is there". Characterisation is transit photometry and spectroscopy, and a + * usable spectrum wants an SNR around 100. Signal-to-noise grows as the square root of the + * photons collected, so the flux ratio between the two is {@code (100/5)² = 400}, and + * {@code 2.5·log10(400) = 6.5} magnitudes.

    + * + *

    What it costs at the shipped aperture, measured: detection reaches 161 ly for a + * sun-like star and 1 359 ly for a blue giant; resolution reaches 8.1 ly and 68 ly. Against a + * mean star separation of 4.23 ly that means an early instrument resolves its nearest few + * neighbours and hands back coordinates for everything else — which is the progression the + * aperture ladder exists to sell.

    + */ + public static final double DEFAULT_TELESCOPE_RESOLVE_MARGIN_MAGNITUDES = 6.5d; /** @see #DEFAULT_TELESCOPE_LIMITING_MAGNITUDE */ public static final int DEFAULT_TELESCOPE_SCAN_MAX_CELLS = 200_000; /** @see #DEFAULT_TELESCOPE_LIMITING_MAGNITUDE */ @@ -310,6 +328,8 @@ public class ARConfiguration { @ConfigProperty public double telescopeConeHalfAngleDegrees; @ConfigProperty + public double telescopeResolveMarginMagnitudes; + @ConfigProperty public int telescopeScanMaxCells; @ConfigProperty public int telescopeScanBaseTicks; @@ -556,6 +576,7 @@ public static void loadPreInit() { arConfig.planetDiscoveryChance = config.get(PLANET, "planetDiscoveryChance", 5, "Chance of planet discovery in the warp controller, chance is 1/n", 1, Integer.MAX_VALUE).getInt(); arConfig.telescopeLimitingMagnitude = config.get(PLANET, "telescopeLimitingMagnitude", DEFAULT_TELESCOPE_LIMITING_MAGNITUDE, "How faint a star an observatory can still register, in APPARENT MAGNITUDE - the scale astronomy measures brightness on, where SMALLER IS BRIGHTER and five magnitudes is a factor of a hundred in received light. This is the instrument's aperture, and it is what its reach is derived FROM: a survey walks outwards only as far as the brightest star it could possibly see would still be above this limit, so a better aperture reaches farther by seeing more rather than by being told a bigger number. Reference points: 6 is roughly the naked eye, 8 (the default) reaches a sun-like star at about 160 light years and a blue giant at 1360, and each 5 magnitudes multiplies every one of those distances by ten. Dust counts against the same limit, so a cloud in the way shortens the reach in exactly the way distance does.", -30d, 40d).getDouble(); arConfig.telescopeConeHalfAngleDegrees = config.get(PLANET, "telescopeConeHalfAngleDegrees", DEFAULT_TELESCOPE_CONE_HALF_ANGLE_DEGREES, "How wide a patch of sky one pointing covers, in DEGREES from the axis to the edge. A survey is a cone with its apex at the observatory, so this is its opening: narrow in degrees, and still enormous at the far end because the same angle subtends more space the farther out it is read. Widening it multiplies the work by the SQUARE, so a pointing twice as wide is four times the survey.", 0.001d, 89d).getDouble(); + arConfig.telescopeResolveMarginMagnitudes = config.get(PLANET, "telescopeResolveMarginMagnitudes", DEFAULT_TELESCOPE_RESOLVE_MARGIN_MAGNITUDES, "How much BRIGHTER than telescopeLimitingMagnitude a system must be before the instrument can make out what is IN it, in magnitudes. Seeing that a point of light is there and measuring what orbits it are not the same observation: detection is called at a signal-to-noise of about 5, while a usable spectrum wants about 100, and since signal-to-noise grows as the square root of the photons collected that is a flux ratio of 400 - i.e. 6.5 magnitudes. Everything the survey registers but cannot resolve is still written down as a POSITION, so a weak instrument hands back a list of places worth flying to and a better one tells you what is at them. Set it to 0 to make anything detectable also resolvable, which is how the survey behaved before the distinction existed.", 0d, 40d).getDouble(); arConfig.telescopeScanMaxCells = config.get(PLANET, "telescopeScanMaxCells", DEFAULT_TELESCOPE_SCAN_MAX_CELLS, "Hard ceiling on how many LOOKS one survey may hold (one per star territory along the pointing, not one per cell of sky crossed). A pointing that would exceed it is SHORTENED until it fits, exactly as its width used to be narrowed - a sweep may be long, but never unbounded. At the shipped aperture and opening a full-depth pointing holds about 77 000 looks, so this leaves room to raise the aperture a little before the ceiling starts cutting the reach.", 1, Integer.MAX_VALUE).getInt(); arConfig.telescopeScanBaseTicks = config.get(PLANET, "telescopeScanBaseTicks", DEFAULT_TELESCOPE_SCAN_BASE_TICKS, "Ticks one STEP of a survey takes. A pointing's cost in time is carried by how many steps it needs and not by how far it reaches, because a deeper pointing already holds proportionally more looks. Only applies with planetsMustBeDiscovered on; without research, an observation is instant.", 0, Integer.MAX_VALUE).getInt(); arConfig.telescopeScanCellsPerStep = config.get(PLANET, "telescopeScanCellsPerStep", DEFAULT_TELESCOPE_SCAN_CELLS_PER_STEP, "How many looks one step of a survey resolves. This is the bound that stops a sweep from enumerating everything at once. With the shipped defaults a full-depth pointing is about 600 steps, i.e. roughly ten minutes of clear night.", 1, Integer.MAX_VALUE).getInt(); diff --git a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java index 2073fb0f2..904d3d02d 100644 --- a/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java +++ b/src/main/java/zmaster587/advancedRocketry/universe/TelescopeScan.java @@ -88,13 +88,35 @@ public static final class Detection { private final double apparentMagnitude; private final double distanceLightYears; private final double extinctionMagnitudes; + private final boolean resolvable; + /** A detection of unstated provenance — always resolvable; see {@link #resolvable()}. */ public Detection(GalacticCoord anchor, double apparentMagnitude, double distanceLightYears, double extinctionMagnitudes) { + this(anchor, apparentMagnitude, distanceLightYears, extinctionMagnitudes, true); + } + + public Detection(GalacticCoord anchor, double apparentMagnitude, double distanceLightYears, + double extinctionMagnitudes, boolean resolvable) { this.anchor = anchor; this.apparentMagnitude = apparentMagnitude; this.distanceLightYears = distanceLightYears; this.extinctionMagnitudes = extinctionMagnitudes; + this.resolvable = resolvable; + } + + /** + * Whether this look was bright enough for the instrument to make out what is IN the system, + * as opposed to merely registering that it is there. + * + *

    Decided where the aperture is known — in {@link #detect} — and carried. It cannot + * be recomputed later against the configured instrument, because the instrument that took + * this look is not necessarily the one the configuration describes: a caller states the limit + * it is observing with, and asking the config afterwards would let those two disagree + * silently. A detection is a fact about a LOOK, and so is this.

    + */ + public boolean resolvable() { + return resolvable; } /** The anchor cell of the system that was registered. */ @@ -120,7 +142,8 @@ public double extinctionMagnitudes() { @Override public String toString() { return "Detection[" + anchor.cellKey() + ", m=" + String.format("%.2f", apparentMagnitude) - + ", " + String.format("%.1f", distanceLightYears) + " ly]"; + + ", " + String.format("%.1f", distanceLightYears) + " ly" + + (resolvable ? "" : ", point only") + "]"; } } @@ -148,6 +171,7 @@ public static List detect(UniverseRegistry registry, GalacticCoord lo return Collections.emptyList(); } List hits = new ArrayList<>(anchors.size()); + double resolveLimit = limitMagnitude - resolveMarginMagnitudes(); for (GalacticCoord anchor : anchors) { if (observer == null) { hits.add(new Detection(anchor, Double.NEGATIVE_INFINITY, 0d, 0d)); @@ -172,7 +196,8 @@ public static List detect(UniverseRegistry registry, GalacticCoord lo double extinction = registry.extinctionBetween(observer, anchor); double magnitude = clearSky + extinction; if (magnitude <= limitMagnitude) { - hits.add(new Detection(anchor, magnitude, lightYears, extinction)); + hits.add(new Detection(anchor, magnitude, lightYears, extinction, + magnitude <= resolveLimit)); } } return hits; @@ -194,6 +219,10 @@ public static List detect(UniverseRegistry registry, GalacticCoord lo * mechanic — a reason to FLY somewhere rather than survey it from home — and it is why * concealment costs detail and never the look itself.

    * + *

    Brightness decides this too, not only the operator. A system registered near the + * aperture's limit is a point of light and nothing more — see {@link #resolveLimitMagnitude()}. + * The operator's choice can only ever ask for LESS than the instrument could have told him.

    + * * @param wholeSystem whether to enumerate the system's bodies, or record the address alone. The * operator's own choice: a full characterisation is the instrument's dear * setting and fills a crystal far faster @@ -209,7 +238,7 @@ public static int characterise(UniverseRegistry registry, Detection hit, Crystal registry.pinSystem(anchor); int written = 0; boolean namedSomething = false; - if (wholeSystem && !isObscuredAt(hit.extinctionMagnitudes())) { + if (wholeSystem && hit.resolvable() && !isObscuredAt(hit.extinctionMagnitudes())) { for (SystemBody body : registry.systemBodiesAt(anchor)) { namedSomething = true; if (memory.record(entryFor(body, observedTick, nameOf))) { @@ -303,6 +332,29 @@ public static double limitMagnitude() { return ARConfiguration.getCurrentConfig().telescopeLimitingMagnitude; } + /** + * How bright a system must be before this instrument can make out what is IN it — the aperture, + * less the margin characterisation costs. + * + *

    Seeing that a point of light is there and measuring what orbits it are two different + * observations, and the second wants far more photons: detection is conventionally called at + * a signal-to-noise of about 5, a usable spectrum at about 100, and signal-to-noise grows as the + * square root of what you collect — a flux ratio of 400, i.e. 6.5 magnitudes + * ({@link ARConfiguration#telescopeResolveMarginMagnitudes}).

    + * + *

    Everything registered but not resolved is still written down as a POSITION. That is the + * whole shape of the mechanic: a weak instrument hands back a list of places worth flying to, + * and a better one — or a visit — says what is at them.

    + */ + public static double resolveLimitMagnitude() { + return limitMagnitude() - resolveMarginMagnitudes(); + } + + /** How much brighter than its detection limit a system must be to be made out. Never negative. */ + public static double resolveMarginMagnitudes() { + return Math.max(0d, ARConfiguration.getCurrentConfig().telescopeResolveMarginMagnitudes); + } + /** * Whether a look from {@code observer} to {@code target} is OBSCURED — a cloud between them thick * enough that a survey can no longer make out what is there, only that something is. diff --git a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeConeSurveyTest.java b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeConeSurveyTest.java index 0648e265f..ddf5ca9d7 100644 --- a/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeConeSurveyTest.java +++ b/src/test/java/zmaster587/advancedRocketry/test/unit/TelescopeConeSurveyTest.java @@ -27,6 +27,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** @@ -48,8 +49,20 @@ public class TelescopeConeSurveyTest { private static final long SEED = 0xC0FFEEL; private static final long STEP = GalaxyGenConfig.DEFAULT_MIN_SPACING; + private double previousMargin; + + @org.junit.Before + public void armResolveMargin() { + previousMargin = ARConfiguration.getCurrentConfig().telescopeResolveMarginMagnitudes; + // STATED, so nothing here depends on the shipped default staying put - except the one test + // that is explicitly about what the shipped default costs, which sets it again itself. + ARConfiguration.getCurrentConfig().telescopeResolveMarginMagnitudes = + ARConfiguration.DEFAULT_TELESCOPE_RESOLVE_MARGIN_MAGNITUDES; + } + @After public void resetSeams() { + ARConfiguration.getCurrentConfig().telescopeResolveMarginMagnitudes = previousMargin; UniverseRegistry.setGenerator(null); UniverseRegistry.setStarLookup(null); } @@ -309,6 +322,96 @@ public void aStarlessWorldIsNotSomethingATelescopeFinds() { TelescopeScan.resolveCell(registry, seat, crystal, 1_000L, id -> "Body-" + id) > 0); } + @Test + public void theOperatorChoosesWhetherADetectionIsFollowedToTheBodies() { + // The instrument's own control, and the reason it is a control rather than a config key: over + // known sky an operator wants every body named, and into sky nobody has visited he wants a + // list of places worth flying to. A deep pointing on FULL fills a crystal many times faster. + // + // Both halves are asserted against the SAME look, because the claim is that the choice is + // what differs — a fixture that only checked the cheap side would pass against an instrument + // that had quietly stopped resolving anything at all. + // Close enough that the instrument could certainly make the system out, so what the test + // measures is the OPERATOR's choice and not the aperture's reach - those are two different + // reasons for a bare row and a fixture near the gate would confuse them. + UniverseRegistry registry = oneStarAt(10d, 1.15f, 100); + GalacticCoord seat = seatAt(10d); + List hits = TelescopeScan.detect(registry, seat, HOME, 12d); + assertTrue("arrangement: the aperture must not be what limits this look", + hits.get(0).resolvable()); + assertEquals("arrangement: exactly one system to follow up", 1, hits.size()); + + CrystalMemory coordsOnly = new CrystalMemory(); + TelescopeScan.characterise(registry, hits.get(0), coordsOnly, 1_000L, id -> "Body-" + id, + false); + assertNull("recording positions only must name no body", coordsOnly.forBody(701)); + assertEquals("but it must still write the address, or the look taught the operator nothing", + 1, coordsOnly.size()); + + CrystalMemory full = new CrystalMemory(); + TelescopeScan.characterise(registry, hits.get(0), full, 1_000L, id -> "Body-" + id, true); + assertNotNull("and the full setting must name the system's bodies", full.forBody(701)); + assertTrue("which is strictly more than the address alone: " + full.size() + " vs " + + coordsOnly.size(), full.size() > coordsOnly.size()); + } + + @Test + public void seeingThatAStarIsThereAndMakingOutWhatOrbitsItAreDifferentObservations() { + // The mechanic the resolve margin buys, and the reason it is not a second aperture: ONE + // instrument, ONE star, and the only thing that differs is how far away it is. Near, the + // survey names the planet; far, it registers a point of light and writes the address. + double sunLike = StellarMagnitude.luminositySuns(1.15d, 100); + double detectAt = 12d; + double resolveAt = detectAt - ARConfiguration.DEFAULT_TELESCOPE_RESOLVE_MARGIN_MAGNITUDES; + double detectReach = StellarMagnitude.detectionRangeLightYears(sunLike, detectAt); + double resolveReach = StellarMagnitude.detectionRangeLightYears(sunLike, resolveAt); + assertTrue("arrangement: resolving must be the harder of the two, by a wide margin: " + + resolveReach + " vs " + detectReach, + resolveReach * 4d < detectReach); + + // Far: inside the aperture, outside what it can make out. + double far = (detectReach + resolveReach) / 2d; + UniverseRegistry farSky = oneStarAt(far, 1.15f, 100); + List farHits = + TelescopeScan.detect(farSky, seatAt(far), HOME, detectAt); + assertEquals("arrangement: it must still REGISTER at this distance", 1, farHits.size()); + assertFalse("but it must not be resolvable", farHits.get(0).resolvable()); + + CrystalMemory distant = new CrystalMemory(); + TelescopeScan.characterise(farSky, farHits.get(0), distant, 1_000L, id -> "Body-" + id, true); + assertNull("so a survey must not name its planet", distant.forBody(701)); + assertEquals("and must still write the address down", 1, distant.size()); + + // Near: the same star, the same instrument, the same request. + double near = resolveReach / 2d; + UniverseRegistry nearSky = oneStarAt(near, 1.15f, 100); + List nearHits = + TelescopeScan.detect(nearSky, seatAt(near), HOME, detectAt); + assertEquals("arrangement: one star to make out", 1, nearHits.size()); + assertTrue("this one must be resolvable", nearHits.get(0).resolvable()); + + CrystalMemory close = new CrystalMemory(); + TelescopeScan.characterise(nearSky, nearHits.get(0), close, 1_000L, id -> "Body-" + id, true); + assertNotNull("and its planet must be named", close.forBody(701)); + } + + @Test + public void theMarginIsTheDifferenceBetweenSeeingAndMEASURING() { + // Where 6.5 comes from, stated as arithmetic so a retune has to argue with the derivation + // rather than with a taste: detection is called at a signal-to-noise of about 5, a usable + // spectrum wants about 100, and signal-to-noise grows as the square root of the photons — + // so the flux ratio is (100/5)^2 = 400, which is 2.5*log10(400) magnitudes. + double fluxRatio = (100d / 5d) * (100d / 5d); + assertEquals("the margin must be the SNR ratio and not a number someone liked", + 2.5d * Math.log10(fluxRatio), + ARConfiguration.DEFAULT_TELESCOPE_RESOLVE_MARGIN_MAGNITUDES, 0.01d); + + // And zero must genuinely turn it off, which is what "disable the flag" has to mean. + ARConfiguration.getCurrentConfig().telescopeResolveMarginMagnitudes = 0d; + assertEquals("a margin of zero makes anything detectable also resolvable", + TelescopeScan.limitMagnitude(), TelescopeScan.resolveLimitMagnitude(), 1e-9d); + } + // ── detection is not characterisation ───────────────────────────────────── /** The real generator, counting the two questions separately. */ @@ -446,6 +549,15 @@ ARConfiguration.DEFAULT_TELESCOPE_LIMITING_MAGNITUDE, archetypes(), shipped.limitMagnitude()).size(); } long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000L; + int resolvable = 0; + for (int i = 0; i < looks; i++) { + for (TelescopeScan.Detection hit : TelescopeScan.detect(registry, scan.cellAt(i), HOME, + shipped.limitMagnitude())) { + if (hit.resolvable()) { + resolvable++; + } + } + } long steps = (looks + shipped.cellsPerStep() - 1) / shipped.cellsPerStep(); System.out.println("the shipped instrument reaches " @@ -453,7 +565,7 @@ ARConfiguration.DEFAULT_TELESCOPE_LIMITING_MAGNITUDE, archetypes(), + shipped.maxRangeSteps() + " territories); a full pointing is " + looks + " looks in " + steps + " steps (" + (steps * shipped.baseTicks() / 20L) + " s of clear night), registered " + detections + " systems, walked in " - + elapsedMs + " ms"); + + elapsedMs + " ms, of which " + resolvable + " were close enough to make out"); assertTrue("a full pointing must stay under the walk ceiling: " + looks, looks <= ARConfiguration.DEFAULT_TELESCOPE_SCAN_MAX_CELLS); From 969ec2078c6be42b5598e5c939b8451c21aa602c Mon Sep 17 00:00:00 2001 From: StannisMod Date: Thu, 20 Aug 2026 11:58:27 +0300 Subject: [PATCH 42/42] Correct the planetDefs reference against what the universe now does - a changed galaxyGen refuses the load and has a defined way through - say what the upgrade cannot reach: crystals nobody could read - one cell is 32 000 000 blocks, not 4 000 000 --- README.md | 6 ++++-- docs/README_PLANETDEFS.md | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e9fa674e1..d64e49daa 100644 --- a/README.md +++ b/README.md @@ -191,8 +191,10 @@ in [`CONTRIBUTING.md`](./CONTRIBUTING.md). > [!CAUTION] > **This line will not load worlds created by the 2.x Advanced Rocketry fork.** The save format changed with -> no migration path. Procedural-universe parameters are the same story: they are inputs to a derived -> universe, so changing one relocates every star and every coordinate a player wrote down. Start a new world. +> no migration path. Procedural-universe parameters are a milder story: they are inputs to a derived +> universe, so changing one moves every system nobody has visited yet — the world therefore refuses to +> open under a changed configuration, and `/stellurgy universe upgrade` is the deliberate way through, +> freezing everything already explored. See the planetDefs reference before you touch them. ## For pack developers diff --git a/docs/README_PLANETDEFS.md b/docs/README_PLANETDEFS.md index dae1d5655..58b63e078 100644 --- a/docs/README_PLANETDEFS.md +++ b/docs/README_PLANETDEFS.md @@ -70,7 +70,7 @@ So: edit the **template**, not the live copy, and keep the template under versio | **planet temperature** | KELVIN | Computed, not authored — see `avgTemperature` in §7. | | **rotational period** | ticks | `24000` = one Minecraft day. Must be `> 0`. | | **star map position** | arbitrary map units | `x` / `y` on ``; affects the star-selector GUI only. | -| **galactic anchor** | cell indices | `"sectorX,sectorY,sectorZ"`, GALAXY-LOCAL (see §5). One cell is 4 000 000 blocks. | +| **galactic anchor** | cell indices | `"sectorX,sectorY,sectorZ"`, GALAXY-LOCAL (see §5). One cell is 32 000 000 blocks. | **The chart scale.** One orbital-distance unit is **5 983 914 blocks**, i.e. one AU is 149 597 870 700 m at 250 m per block. This is the one law that turns an orbit into a place, and it is @@ -219,15 +219,38 @@ against 5 000 AU of room, a factor of nearly nine. It becomes reachable only if more than two orders of magnitude — below roughly 170 000 cells systems start losing outer worlds, and below about 8 cells only the star survives. -### Changing a `` parameter mid-save is UNDEFINED +### Changing a `` parameter mid-save is a PROCEDURE, not an edit `density`, `minSpacing`, `galaxySpacing` and `galaxyDensity` are inputs to a **derived** universe: nothing about a procedural system is stored, so changing any of them relocates every star, every -planet and every generated name. **You get a different universe, and anything a player recorded about -the old one — coordinates, memory crystals, a route — points at nothing.** - -There is no migration and there cannot be one: there is no old universe on disk to migrate. If you -change these, start a new world. +planet and every generated name that nobody has looked at yet. + +**The world refuses to open under a changed configuration.** The save carries a fingerprint of the +`` it was generated under; on a mismatch the load stops and names both fingerprints, +rather than quietly handing the players a different sky. So the failure mode is a server that will +not start, never a route that silently stops leading anywhere. + +**There is a way through, and it keeps what has been explored.** In order: + +1. Restore the previous `` and start the world (§1: in an existing world a template edit + reaches the live copy only through `resetPlanetsFromXML`, which resets itself after one load + unless `ResetOnlyOnce` is `false`). +2. Run `/stellurgy universe upgrade confirm`. Every system anybody has already seen is frozen where + it stands, including the addresses on the memory crystals of players who are **online at that + moment**. +3. Stop the server, install the new configuration, and start again. The stamp is accepted once, and + only if the configuration actually moved. + +The result is a seam at the frontier of the explored: charted space keeps exactly what it held, +unexplored space is re-derived under the new parameters. + +**What the procedure cannot reach.** A crystal in a chest, in an unloaded chunk, or in the inventory +of an offline player is not readable at step 2, so the addresses on it are not frozen. After the +upgrade such an address still resolves — it is a lattice coordinate — but it names whatever the new +universe puts in that cell, which is usually not what the player wrote down. Bring the crystals that +matter to somebody online before running it. + +**Starting a new world is still the simpler answer** if nothing has been explored yet. ---