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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/src/main/java/net/onelitefeather/titan/app/Titan.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@
import net.onelitefeather.titan.common.event.EntityDismountEvent;
import net.onelitefeather.titan.common.helper.BlockHandlerHelper;
import net.onelitefeather.titan.common.map.MapProvider;
import net.onelitefeather.titan.common.time.SeasonService;
import net.onelitefeather.titan.common.time.TimeConfigProvider;
import net.onelitefeather.titan.common.time.TitanTime;
import net.onelitefeather.titan.common.time.WorldTimeService;
import net.onelitefeather.titan.common.utils.Cancelable;

import java.nio.file.Path;
import java.time.Clock;

public final class Titan {

Expand All @@ -49,6 +54,8 @@ public final class Titan {
private final MapProvider mapProvider;
private final AppConfigProvider appConfigProvider;
private final NavigationHelper navigationHelper;
private final WorldTimeService worldTimeService;
private final SeasonService seasonService;

public Titan() {
MinecraftServer.getConnectionManager().setPlayerProvider(TitanPlayer::new);
Expand All @@ -59,6 +66,15 @@ public Titan() {
this.mapProvider = MapProvider.create(this.path, instance);
this.appConfigProvider = AppConfigProvider.create(this.path);
this.navigationHelper = NavigationHelper.instance(this.deliver);
// The lobby tells its time in Berlin, whatever zone the host machine is set to.
Clock clock = Clock.system(TitanTime.EDITORIAL_ZONE);
// Which mapping and which season boundaries run is a question for time.json, not for this
// file: it names no strategy at all (US-2.07, US-2.12, US-2.13). A value the file gets
// wrong stops the boot here rather than running a strategy nobody chose.
TimeConfigProvider timeConfigProvider = TimeConfigProvider.create(this.path, TitanTime.EDITORIAL_ZONE);
this.worldTimeService = timeConfigProvider.createWorldTimeService(clock);
this.seasonService = timeConfigProvider.createSeasonService(clock);
this.worldTimeService.bind(instance);
}

public void initialize() {
Expand All @@ -71,7 +87,25 @@ public void initialize() {
}

public void terminate() {
this.worldTimeService.unbind();
}

/**
* Returns the service that drives the lobby's day time from real time.
*
* @return the world time service
*/
public WorldTimeService worldTimeService() {
return this.worldTimeService;
}

/**
* Returns the service that tells which season the lobby is in.
*
* @return the season service
*/
public SeasonService seasonService() {
return this.seasonService;
}

private void initCommands() {
Expand Down
1 change: 1 addition & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
testImplementation(libs.cyano)
testImplementation(libs.aves)
testImplementation(libs.junit.api)
testImplementation(libs.junit.params)
testImplementation(libs.junit.platform.launcher)
testRuntimeOnly(libs.junit.engine)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Copyright (C) 2025 OneLiteFeather Network
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.onelitefeather.titan.common.time;

import org.jetbrains.annotations.Contract;

import java.time.Instant;
import java.time.ZoneId;

/**
* Maps an instant of the real world onto the day time of a Minecraft world.
*
* <p>Implementations are stateless and pure: they are handed the instant instead of reading a clock
* themselves. The {@link java.time.Clock} lives in the calling {@link WorldTimeService} (US-2.03),
* which is what makes every mapping testable against fixed instants without waiting for real time.
*
* @author TheMeinerLP
* @version 1.0.0
* @since 1.15.0
*/
public interface DayTimeStrategy {

/** A full Minecraft day in ticks. */
int TICKS_PER_DAY = 24_000;

/**
* The tick at which the sun stands at its highest point in Minecraft.
*
* <p>Minecraft counts a day from sunrise: tick {@code 0} is daybreak, {@code 6000} noon,
* {@code 12000} nightfall and {@code 18000} midnight.
*/
int NOON_TICK = 6_000;

/** The first tick of the Minecraft night; the day segment is {@code [0, DUSK_TICK)}. */
int DUSK_TICK = 12_000;

/**
* Returns the day time that applies at the given instant.
*
* @param instant the instant the day time applies to
* @param zone the time zone that is calculated against
* @return the day time in ticks, within {@code [0, }{@value #TICKS_PER_DAY}{@code )}
*/
@Contract(pure = true)
long ticksAt(Instant instant, ZoneId zone);

/**
* Returns the linear mapping, the default of stage 2 (US-2.06).
*
* @return the shared, immutable linear strategy
*/
@Contract(pure = true)
static DayTimeStrategy linear() {
return LinearDayTimeStrategy.instance();
}

/**
* Returns the solar mapping for Berlin (US-2.07).
*
* @return the shared, immutable solar strategy for Berlin
*/
@Contract(pure = true)
static DayTimeStrategy solar() {
return SolarDayTimeStrategy.berlin();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright (C) 2025 OneLiteFeather Network
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.onelitefeather.titan.common.time;

import org.jetbrains.annotations.Contract;

import java.time.Instant;
import java.time.ZoneId;

/**
* Spreads 24 real hours evenly over the {@value DayTimeStrategy#TICKS_PER_DAY} ticks of a Minecraft
* day, so that {@code 12:00} local time is noon in the world.
*
* <p>This is the default of stage 2 (US-2.06). It delivers nearly the whole benefit of a real-time
* lobby and has no astronomical calculation that could be silently wrong.
*
* <p>The mapping reads the <em>local wall clock</em>, not the offset from UTC. That is deliberate:
* on the last Sunday in March the lobby jumps forward by 1000 ticks together with everybody's
* watch,
* and on the last Sunday in October it repeats the hour. Noon in the world is whenever a player in
* Berlin says it is noon (US-2.04, NFR-006).
*
* @author TheMeinerLP
* @version 1.0.0
* @since 1.15.0
*/
public final class LinearDayTimeStrategy implements DayTimeStrategy {

private static final LinearDayTimeStrategy INSTANCE = new LinearDayTimeStrategy();

private static final int SECONDS_PER_DAY = 86_400;

/**
* The wall clock second that Minecraft tick {@code 0} stands for.
*
* <p>Tick {@code 0} is daybreak, which the game presents as 06:00.
*/
private static final int DAYBREAK_SECOND_OF_DAY = 6 * 3_600;

private LinearDayTimeStrategy() {
}

/**
* Returns the shared instance.
*
* <p>The strategy carries no state, so a single instance serves every caller.
*
* @return the shared linear strategy
*/
@Contract(pure = true)
public static LinearDayTimeStrategy instance() {
return INSTANCE;
}

@Override
@Contract(pure = true)
public long ticksAt(Instant instant, ZoneId zone) {
int secondOfDay = instant.atZone(zone).toLocalTime().toSecondOfDay();
long sinceDaybreak = Math.floorMod(secondOfDay - DAYBREAK_SECOND_OF_DAY, (long) SECONDS_PER_DAY);
return sinceDaybreak * TICKS_PER_DAY / SECONDS_PER_DAY;
}

@Override
public String toString() {
return "LinearDayTimeStrategy";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright (C) 2025 OneLiteFeather Network
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.onelitefeather.titan.common.time;

import net.onelitefeather.titan.common.time.season.Season;
import net.onelitefeather.titan.common.time.season.SeasonBoundaryStrategy;
import org.jetbrains.annotations.Contract;

import java.time.Clock;
import java.time.LocalDate;
import java.time.ZoneId;

/**
* Provides the season the lobby is currently in as read-only state (US-2.09).
*
* <p>Like {@link WorldTimeService} it holds the {@link Clock} that the strategies deliberately do
* not, so that "which season is it" can be asked for any instant a test cares to name (US-2.03).
*
* @author TheMeinerLP
* @version 1.0.0
* @since 1.15.0
*/
public interface SeasonService {

/**
* Creates a service on the meteorological boundaries in the editorial zone — the stage 2
* default.
*
* @param clock the clock the current date is read from
* @return a new service
*/
@Contract(pure = true, value = "_ -> new")
static SeasonService create(Clock clock) {
return create(clock, TitanTime.EDITORIAL_ZONE, SeasonBoundaryStrategy.meteorological());
}

/**
* Creates a service on the given boundaries in the editorial zone.
*
* @param clock the clock the current date is read from
* @param strategy the boundaries to apply
* @return a new service
*/
@Contract(pure = true, value = "_, _ -> new")
static SeasonService create(Clock clock, SeasonBoundaryStrategy strategy) {
return create(clock, TitanTime.EDITORIAL_ZONE, strategy);
}

/**
* Creates a service on the given boundaries in the given zone.
*
* @param clock the clock the current date is read from
* @param zone the zone the date is read in
* @param strategy the boundaries to apply
* @return a new service
*/
@Contract(pure = true, value = "_, _, _ -> new")
static SeasonService create(Clock clock, ZoneId zone, SeasonBoundaryStrategy strategy) {
return new TitanSeasonService(clock, zone, strategy);
}

/**
* Returns the season in effect at the clock's current instant.
*
* @return the current season
*/
Season currentSeason();

/**
* Returns the date the clock's current instant falls on in this service's zone.
*
* @return the current date
*/
LocalDate currentDate();

/**
* Returns the zone the date is read in.
*
* @return the zone
*/
@Contract(pure = true)
ZoneId zone();

/**
* Returns the boundaries in use.
*
* @return the strategy
*/
@Contract(pure = true)
SeasonBoundaryStrategy strategy();
}
Loading