├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .gitignore ├── apiserver ├── src │ └── main │ │ └── java │ │ └── micheal65536 │ │ └── vienna │ │ └── apiserver │ │ ├── types │ │ ├── workshop │ │ │ ├── BoostState.java │ │ │ ├── UnlockPrice.java │ │ │ ├── OutputItem.java │ │ │ ├── FinishPrice.java │ │ │ ├── InputItem.java │ │ │ ├── State.java │ │ │ ├── CraftingSlot.java │ │ │ └── SmeltingSlot.java │ │ ├── buildplates │ │ │ ├── Dimension.java │ │ │ ├── Offset.java │ │ │ ├── SurfaceOrientation.java │ │ │ ├── OwnedBuildplate.java │ │ │ ├── SharedBuildplate.java │ │ │ └── BuildplateInstance.java │ │ ├── common │ │ │ ├── BurnRate.java │ │ │ ├── Coordinate.java │ │ │ ├── ExpectedPurchasePrice.java │ │ │ ├── Rarity.java │ │ │ ├── Effect.java │ │ │ ├── Token.java │ │ │ └── Rewards.java │ │ ├── profile │ │ │ ├── SplitRubies.java │ │ │ └── Profile.java │ │ ├── inventory │ │ │ ├── Inventory.java │ │ │ ├── HotbarItem.java │ │ │ ├── StackableInventoryItem.java │ │ │ └── NonStackableInventoryItem.java │ │ ├── tappables │ │ │ ├── EncounterState.java │ │ │ └── ActiveLocation.java │ │ ├── catalog │ │ │ ├── NFCBoost.java │ │ │ ├── JournalCatalog.java │ │ │ ├── BoostMetadata.java │ │ │ ├── RecipesCatalog.java │ │ │ └── ItemsCatalog.java │ │ ├── journal │ │ │ └── Journal.java │ │ └── boosts │ │ │ └── Boosts.java │ │ ├── routing │ │ ├── Handler.java │ │ ├── Filter.java │ │ ├── ServerErrorException.java │ │ ├── Path.java │ │ ├── Application.java │ │ ├── Response.java │ │ └── Request.java │ │ ├── utils │ │ ├── MapBuilder.java │ │ ├── ItemWear.java │ │ ├── ActivityLogUtils.java │ │ ├── TimeFormatter.java │ │ ├── EarthApiResponse.java │ │ ├── LevelUtils.java │ │ ├── TokenUtils.java │ │ └── CraftingCalculator.java │ │ └── routes │ │ ├── ResourcePacksRouter.java │ │ ├── SigninRouter.java │ │ ├── PlayerRouter.java │ │ ├── AuthenticatedRouter.java │ │ ├── player │ │ ├── ChallengesRouter.java │ │ ├── JournalRouter.java │ │ ├── TokensRouter.java │ │ └── ProfileRouter.java │ │ └── EnvironmentSettingsRouter.java └── pom.xml ├── db ├── src │ └── main │ │ └── java │ │ └── micheal65536 │ │ └── vienna │ │ └── db │ │ ├── model │ │ ├── common │ │ │ ├── NonStackableItemInstance.java │ │ │ └── Rewards.java │ │ ├── player │ │ │ ├── workshop │ │ │ │ ├── InputItem.java │ │ │ │ ├── CraftingSlots.java │ │ │ │ ├── SmeltingSlots.java │ │ │ │ ├── CraftingSlot.java │ │ │ │ └── SmeltingSlot.java │ │ │ ├── Profile.java │ │ │ ├── RedeemedTappables.java │ │ │ ├── Rubies.java │ │ │ ├── Boosts.java │ │ │ ├── Journal.java │ │ │ ├── Buildplates.java │ │ │ ├── Hotbar.java │ │ │ ├── Tokens.java │ │ │ ├── ActivityLog.java │ │ │ └── Inventory.java │ │ └── global │ │ │ ├── EncounterBuildplates.java │ │ │ └── SharedBuildplates.java │ │ └── DatabaseException.java └── pom.xml ├── utils ├── http-routing │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── micheal65536 │ │ │ └── vienna │ │ │ └── utils │ │ │ └── http │ │ │ └── routing │ │ │ ├── Handler.java │ │ │ ├── Filter.java │ │ │ ├── ServerErrorException.java │ │ │ ├── Path.java │ │ │ ├── Application.java │ │ │ ├── Response.java │ │ │ └── Request.java │ └── pom.xml ├── cdn │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── micheal65536 │ │ └── vienna │ │ └── utils │ │ └── cdn │ │ └── Main.java ├── locator │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── micheal65536 │ │ └── vienna │ │ └── utils │ │ └── locator │ │ └── Main.java └── tools │ └── buildplate-importer │ └── pom.xml ├── eventbus ├── client │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── micheal65536 │ │ │ └── vienna │ │ │ └── eventbus │ │ │ └── client │ │ │ ├── EventBusClientException.java │ │ │ ├── Subscriber.java │ │ │ ├── RequestHandler.java │ │ │ ├── Publisher.java │ │ │ └── RequestSender.java │ └── pom.xml └── server │ ├── src │ └── main │ │ └── java │ │ └── micheal65536 │ │ └── vienna │ │ └── eventbus │ │ └── server │ │ └── Main.java │ └── pom.xml ├── objectstore ├── client │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── micheal65536 │ │ │ └── vienna │ │ │ └── objectstore │ │ │ └── client │ │ │ └── ObjectStoreClientException.java │ └── pom.xml └── server │ ├── src │ └── main │ │ └── java │ │ └── micheal65536 │ │ └── vienna │ │ └── objectstore │ │ └── server │ │ ├── Server.java │ │ ├── Main.java │ │ └── DataStore.java │ └── pom.xml ├── staticdata ├── src │ └── main │ │ └── java │ │ └── micheal65536 │ │ └── vienna │ │ └── staticdata │ │ ├── StaticDataException.java │ │ ├── StaticData.java │ │ ├── EncountersConfig.java │ │ ├── Levels.java │ │ └── TappablesConfig.java └── pom.xml ├── tappablesgenerator ├── src │ └── main │ │ └── java │ │ └── micheal65536 │ │ └── vienna │ │ └── tappablesgenerator │ │ ├── Encounter.java │ │ ├── Tappable.java │ │ ├── EncounterGenerator.java │ │ ├── Main.java │ │ ├── TappableGenerator.java │ │ └── ActiveTiles.java └── pom.xml └── pom.xml /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Genoa/Vienna/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*/target 2 | /buildplate/*/target 3 | /eventbus/*/target 4 | /objectstore/*/target 5 | /utils/*/target 6 | /utils/*/*/target 7 | /.idea 8 | /.vscode 9 | -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/workshop/BoostState.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.workshop; 2 | 3 | public record BoostState( 4 | // TODO 5 | ) 6 | { 7 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/buildplates/Dimension.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.buildplates; 2 | 3 | public record Dimension( 4 | int x, 5 | int z 6 | ) 7 | { 8 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/buildplates/Offset.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.buildplates; 2 | 3 | public record Offset( 4 | int x, 5 | int y, 6 | int z 7 | ) 8 | { 9 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/common/BurnRate.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.common; 2 | 3 | public record BurnRate( 4 | int burnTime, 5 | int heatPerSecond 6 | ) 7 | { 8 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/profile/SplitRubies.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.profile; 2 | 3 | public record SplitRubies( 4 | int purchased, 5 | int earned 6 | ) 7 | { 8 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/workshop/UnlockPrice.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.workshop; 2 | 3 | public record UnlockPrice( 4 | int cost, 5 | int discount 6 | ) 7 | { 8 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/common/Coordinate.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.common; 2 | 3 | public record Coordinate( 4 | float latitude, 5 | float longitude 6 | ) 7 | { 8 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/common/ExpectedPurchasePrice.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.common; 2 | 3 | public record ExpectedPurchasePrice( 4 | int expectedPurchasePrice 5 | ) 6 | { 7 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/workshop/OutputItem.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record OutputItem( 6 | @NotNull String itemId, 7 | int quantity 8 | ) 9 | { 10 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/common/NonStackableItemInstance.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.common; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record NonStackableItemInstance( 6 | @NotNull String instanceId, 7 | int wear 8 | ) 9 | { 10 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/workshop/FinishPrice.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record FinishPrice( 6 | int cost, 7 | int discount, 8 | @NotNull String validTime 9 | ) 10 | { 11 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/workshop/InputItem.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record InputItem( 6 | @NotNull String itemId, 7 | int quantity, 8 | String[] instanceIds 9 | ) 10 | { 11 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routing/Handler.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routing; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public interface Handler 6 | { 7 | @NotNull 8 | Response handle(@NotNull Request request) throws Request.BadRequestException, ServerErrorException; 9 | } -------------------------------------------------------------------------------- /utils/http-routing/src/main/java/micheal65536/vienna/utils/http/routing/Handler.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.utils.http.routing; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public interface Handler 6 | { 7 | @NotNull 8 | Response handle(@NotNull Request request) throws Request.BadRequestException, ServerErrorException; 9 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/buildplates/SurfaceOrientation.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.buildplates; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public enum SurfaceOrientation 6 | { 7 | @SerializedName("Horizontal") HORIZONTAL, 8 | @SerializedName("Vertical") VERTICAL // TODO: unverified 9 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routing/Filter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routing; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public interface Filter 7 | { 8 | @Nullable 9 | Response filter(@NotNull Request request) throws Request.BadRequestException, ServerErrorException; 10 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/inventory/Inventory.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.inventory; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record Inventory( 6 | HotbarItem[] hotbar, 7 | @NotNull StackableInventoryItem[] stackableItems, 8 | @NotNull NonStackableInventoryItem[] nonStackableItems 9 | ) 10 | { 11 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/workshop/State.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.workshop; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public enum State 6 | { 7 | @SerializedName("Empty") EMPTY, 8 | @SerializedName("Active") ACTIVE, 9 | @SerializedName("Completed") COMPLETED, 10 | @SerializedName("Locked") LOCKED 11 | } -------------------------------------------------------------------------------- /utils/http-routing/src/main/java/micheal65536/vienna/utils/http/routing/Filter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.utils.http.routing; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public interface Filter 7 | { 8 | @Nullable 9 | Response filter(@NotNull Request request) throws Request.BadRequestException, ServerErrorException; 10 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/inventory/HotbarItem.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.inventory; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public record HotbarItem( 7 | @NotNull String id, 8 | int count, 9 | @Nullable String instanceId, 10 | @Nullable Float health 11 | ) 12 | { 13 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/workshop/InputItem.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.db.model.common.NonStackableItemInstance; 6 | 7 | public record InputItem( 8 | @NotNull String id, 9 | int count, 10 | @NotNull NonStackableItemInstance[] instances 11 | ) 12 | { 13 | } -------------------------------------------------------------------------------- /eventbus/client/src/main/java/micheal65536/vienna/eventbus/client/EventBusClientException.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.eventbus.client; 2 | 3 | public class EventBusClientException extends Exception 4 | { 5 | EventBusClientException(String message) 6 | { 7 | super(message); 8 | } 9 | 10 | EventBusClientException(String message, Throwable cause) 11 | { 12 | super(message, cause); 13 | } 14 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routing/ServerErrorException.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routing; 2 | 3 | public final class ServerErrorException extends Exception 4 | { 5 | public ServerErrorException(String message, Throwable cause) 6 | { 7 | super(message, cause); 8 | } 9 | 10 | public ServerErrorException(Throwable cause) 11 | { 12 | super(cause); 13 | } 14 | } -------------------------------------------------------------------------------- /utils/http-routing/src/main/java/micheal65536/vienna/utils/http/routing/ServerErrorException.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.utils.http.routing; 2 | 3 | public final class ServerErrorException extends Exception 4 | { 5 | public ServerErrorException(String message, Throwable cause) 6 | { 7 | super(message, cause); 8 | } 9 | 10 | public ServerErrorException(Throwable cause) 11 | { 12 | super(cause); 13 | } 14 | } -------------------------------------------------------------------------------- /objectstore/client/src/main/java/micheal65536/vienna/objectstore/client/ObjectStoreClientException.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.objectstore.client; 2 | 3 | public class ObjectStoreClientException extends Exception 4 | { 5 | ObjectStoreClientException(String message) 6 | { 7 | super(message); 8 | } 9 | 10 | ObjectStoreClientException(String message, Throwable cause) 11 | { 12 | super(message, cause); 13 | } 14 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/DatabaseException.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db; 2 | 3 | public final class DatabaseException extends Exception 4 | { 5 | DatabaseException(String message) 6 | { 7 | super(message); 8 | } 9 | 10 | DatabaseException(String message, Throwable cause) 11 | { 12 | super(message, cause); 13 | } 14 | 15 | DatabaseException(Throwable cause) 16 | { 17 | super(cause); 18 | } 19 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/workshop/CraftingSlots.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public final class CraftingSlots 6 | { 7 | @NotNull 8 | public final CraftingSlot[] slots; 9 | 10 | public CraftingSlots() 11 | { 12 | this.slots = new CraftingSlot[]{new CraftingSlot(), new CraftingSlot(), new CraftingSlot()}; 13 | } 14 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/workshop/SmeltingSlots.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public final class SmeltingSlots 6 | { 7 | @NotNull 8 | public final SmeltingSlot[] slots; 9 | 10 | public SmeltingSlots() 11 | { 12 | this.slots = new SmeltingSlot[]{new SmeltingSlot(), new SmeltingSlot(), new SmeltingSlot()}; 13 | } 14 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/common/Rarity.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.common; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public enum Rarity 6 | { 7 | @SerializedName("Common") COMMON, 8 | @SerializedName("Uncommon") UNCOMMON, 9 | @SerializedName("Rare") RARE, 10 | @SerializedName("Epic") EPIC, 11 | @SerializedName("Legendary") LEGENDARY, 12 | @SerializedName("oobe") OOBE 13 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/inventory/StackableInventoryItem.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.inventory; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record StackableInventoryItem( 6 | @NotNull String id, 7 | int owned, 8 | int fragments, 9 | @NotNull On unlocked, 10 | @NotNull On seen 11 | ) 12 | { 13 | public record On( 14 | @NotNull String on 15 | ) 16 | { 17 | } 18 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/Profile.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public final class Profile 6 | { 7 | public int health; 8 | public int experience; 9 | public int level; 10 | @NotNull 11 | public final Rubies rubies; 12 | 13 | public Profile() 14 | { 15 | this.health = 20; 16 | this.experience = 0; 17 | this.level = 1; 18 | this.rubies = new Rubies(); 19 | } 20 | } -------------------------------------------------------------------------------- /staticdata/src/main/java/micheal65536/vienna/staticdata/StaticDataException.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.staticdata; 2 | 3 | public class StaticDataException extends Exception 4 | { 5 | public StaticDataException(String message) 6 | { 7 | super(message); 8 | } 9 | 10 | public StaticDataException(Throwable cause) 11 | { 12 | super(cause); 13 | } 14 | 15 | public StaticDataException(String message, Throwable cause) 16 | { 17 | super(message, cause); 18 | } 19 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/common/Rewards.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.common; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | 8 | public record Rewards( 9 | int rubies, 10 | int experiencePoints, 11 | @Nullable Integer level, 12 | @NotNull HashMap items, 13 | @NotNull String[] buildplates, 14 | @NotNull String[] challenges 15 | ) 16 | { 17 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/tappables/EncounterState.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.tappables; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public record EncounterState( 7 | @NotNull ActiveEncounterState activeEncounterState 8 | ) 9 | { 10 | public enum ActiveEncounterState 11 | { 12 | @SerializedName("Pristine") PRISTINE, 13 | @SerializedName("Dirty") DIRTY 14 | } 15 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/catalog/NFCBoost.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.catalog; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.apiserver.types.common.Rewards; 6 | 7 | public record NFCBoost( 8 | @NotNull String id, 9 | @NotNull String name, 10 | @NotNull String type, 11 | @NotNull Rewards rewards, 12 | @NotNull BoostMetadata boostMetadata, 13 | boolean deprecated, 14 | @NotNull String toolsVersion 15 | ) 16 | { 17 | } -------------------------------------------------------------------------------- /tappablesgenerator/src/main/java/micheal65536/vienna/tappablesgenerator/Encounter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.tappablesgenerator; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record Encounter( 6 | @NotNull String id, 7 | float lat, 8 | float lon, 9 | long spawnTime, 10 | long validFor, 11 | @NotNull String icon, 12 | @NotNull Rarity rarity, 13 | @NotNull String encounterBuildplateId 14 | ) 15 | { 16 | public enum Rarity 17 | { 18 | COMMON, 19 | UNCOMMON, 20 | RARE, 21 | EPIC, 22 | LEGENDARY 23 | } 24 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/common/Effect.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.common; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public record Effect( 7 | @NotNull String type, 8 | @Nullable String duration, 9 | @Nullable Integer value, 10 | @Nullable String unit, 11 | @NotNull String targets, 12 | @NotNull String[] items, 13 | @NotNull String[] itemScenarios, 14 | @NotNull String activation, 15 | @Nullable String modifiesType 16 | ) 17 | { 18 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/inventory/NonStackableInventoryItem.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.inventory; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record NonStackableInventoryItem( 6 | @NotNull String id, 7 | @NotNull Instance[] instances, 8 | int fragments, 9 | @NotNull On unlocked, 10 | @NotNull On seen 11 | ) 12 | { 13 | public record Instance( 14 | @NotNull String id, 15 | float health 16 | ) 17 | { 18 | } 19 | 20 | public record On( 21 | @NotNull String on 22 | ) 23 | { 24 | } 25 | } -------------------------------------------------------------------------------- /tappablesgenerator/src/main/java/micheal65536/vienna/tappablesgenerator/Tappable.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.tappablesgenerator; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record Tappable( 6 | @NotNull String id, 7 | float lat, 8 | float lon, 9 | long spawnTime, 10 | long validFor, 11 | @NotNull String icon, 12 | @NotNull Rarity rarity, 13 | @NotNull Item[] items 14 | ) 15 | { 16 | public enum Rarity 17 | { 18 | COMMON, 19 | UNCOMMON, 20 | RARE, 21 | EPIC, 22 | LEGENDARY 23 | } 24 | 25 | public record Item( 26 | @NotNull String id, 27 | int count 28 | ) 29 | { 30 | } 31 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/catalog/JournalCatalog.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.catalog; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | 8 | public record JournalCatalog( 9 | @NotNull HashMap items 10 | ) 11 | { 12 | public record Item( 13 | @NotNull String referenceId, 14 | @NotNull String parentCollection, 15 | int overallOrder, 16 | int collectionOrder, 17 | @Nullable String defaultSound, 18 | boolean deprecated, 19 | @NotNull String toolsVersion 20 | ) 21 | { 22 | } 23 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/profile/Profile.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.profile; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.apiserver.types.common.Rewards; 6 | 7 | import java.util.HashMap; 8 | 9 | public record Profile( 10 | @NotNull HashMap levelDistribution, 11 | int totalExperience, 12 | int level, 13 | int currentLevelExperience, 14 | int experienceRemaining, 15 | int health, 16 | float healthPercentage 17 | ) 18 | { 19 | public record Level( 20 | int experienceRequired, 21 | @NotNull Rewards rewards 22 | ) 23 | { 24 | } 25 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/utils/MapBuilder.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.utils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | 8 | public final class MapBuilder 9 | { 10 | private final HashMap map = new HashMap<>(); 11 | 12 | public MapBuilder() 13 | { 14 | // empty 15 | } 16 | 17 | @NotNull 18 | public MapBuilder put(@NotNull K name, @Nullable V value) 19 | { 20 | this.map.put(name, value); 21 | return this; 22 | } 23 | 24 | @NotNull 25 | public HashMap getMap() 26 | { 27 | return this.map; 28 | } 29 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/workshop/CraftingSlot.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public record CraftingSlot( 7 | @Nullable String sessionId, 8 | @Nullable String recipeId, 9 | @Nullable OutputItem output, 10 | InputItem[] escrow, 11 | int completed, 12 | int available, 13 | int total, 14 | @Nullable String nextCompletionUtc, 15 | @Nullable String totalCompletionUtc, 16 | @NotNull State state, 17 | @Nullable BoostState boostState, 18 | @Nullable UnlockPrice unlockPrice, 19 | int streamVersion 20 | ) 21 | { 22 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/catalog/BoostMetadata.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.catalog; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import micheal65536.vienna.apiserver.types.common.Effect; 7 | 8 | public record BoostMetadata( 9 | @NotNull String name, 10 | @NotNull String type, 11 | @NotNull String attribute, 12 | boolean canBeDeactivated, 13 | boolean canBeRemoved, 14 | @Nullable String activeDuration, 15 | boolean additive, 16 | @Nullable Integer level, 17 | @NotNull Effect[] effects, 18 | @Nullable String scenario, 19 | @Nullable String cooldown 20 | ) 21 | { 22 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/workshop/CraftingSlot.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public final class CraftingSlot 7 | { 8 | @Nullable 9 | public ActiveJob activeJob; 10 | public boolean locked; 11 | 12 | public CraftingSlot() 13 | { 14 | this.activeJob = null; 15 | this.locked = false; 16 | } 17 | 18 | public record ActiveJob( 19 | @NotNull String sessionId, 20 | @NotNull String recipeId, 21 | long startTime, 22 | @NotNull InputItem[][] input, 23 | int totalRounds, 24 | int collectedRounds, 25 | boolean finishedEarly 26 | ) 27 | { 28 | } 29 | } -------------------------------------------------------------------------------- /staticdata/src/main/java/micheal65536/vienna/staticdata/StaticData.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.staticdata; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.File; 6 | 7 | public final class StaticData 8 | { 9 | public final Catalog catalog; 10 | public final Levels levels; 11 | public final TappablesConfig tappablesConfig; 12 | public final EncountersConfig encountersConfig; 13 | 14 | public StaticData(@NotNull File dir) throws StaticDataException 15 | { 16 | this.catalog = new Catalog(new File(dir, "catalog")); 17 | this.levels = new Levels(new File(dir, "levels")); 18 | this.tappablesConfig = new TappablesConfig(new File(dir, "tappables")); 19 | this.encountersConfig = new EncountersConfig(new File(dir, "encounters")); 20 | } 21 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/RedeemedTappables.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.HashMap; 6 | 7 | public final class RedeemedTappables 8 | { 9 | private final HashMap tappables = new HashMap<>(); 10 | 11 | public RedeemedTappables() 12 | { 13 | // empty 14 | } 15 | 16 | public boolean isRedeemed(@NotNull String id) 17 | { 18 | return this.tappables.containsKey(id); 19 | } 20 | 21 | public void add(@NotNull String id, long expiresAt) 22 | { 23 | this.tappables.put(id, expiresAt); 24 | } 25 | 26 | public void prune(long currentTime) 27 | { 28 | this.tappables.entrySet().removeIf(entry -> entry.getValue() < currentTime); 29 | } 30 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/common/Token.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.common; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.util.HashMap; 7 | 8 | public record Token( 9 | @NotNull Type clientType, 10 | @NotNull HashMap clientProperties, 11 | @NotNull Rewards rewards, 12 | @NotNull Lifetime lifetime 13 | ) 14 | { 15 | public enum Type 16 | { 17 | @SerializedName("adv_zyki") LEVEL_UP, 18 | @SerializedName("redeemtappable") TAPPABLE, 19 | @SerializedName("item.unlocked") JOURNAL_ITEM_UNLOCKED 20 | } 21 | 22 | public enum Lifetime 23 | { 24 | @SerializedName("Persistent") PERSISTENT, 25 | @SerializedName("Transient") TRANSIENT 26 | } 27 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/buildplates/OwnedBuildplate.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.buildplates; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public record OwnedBuildplate( 7 | @NotNull String id, 8 | @NotNull String templateId, 9 | @NotNull Dimension dimension, 10 | @NotNull Offset offset, 11 | int blocksPerMeter, 12 | @NotNull Type type, 13 | @NotNull SurfaceOrientation surfaceOrientation, 14 | @NotNull String model, 15 | int order, 16 | boolean locked, 17 | int requiredLevel, 18 | boolean isModified, 19 | @NotNull String lastUpdated, 20 | int numberOfBlocks, 21 | @NotNull String eTag 22 | ) 23 | { 24 | public enum Type 25 | { 26 | @SerializedName("Survival") SURVIVAL 27 | } 28 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/utils/ItemWear.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.utils; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import micheal65536.vienna.staticdata.Catalog; 7 | 8 | public final class ItemWear 9 | { 10 | public static float wearToHealth(@NotNull String itemId, int wear, @NotNull Catalog.ItemsCatalog itemsCatalog) 11 | { 12 | Catalog.ItemsCatalog.Item catalogItem = itemsCatalog.getItem(itemId); 13 | if (catalogItem == null || catalogItem.toolInfo() == null) 14 | { 15 | LogManager.getLogger().warn("Attempt to get item health for non-tool item {}", itemId); 16 | return 100.0f; 17 | } 18 | return ((float) (catalogItem.toolInfo().maxWear() - wear) / (float) catalogItem.toolInfo().maxWear()) * 100.0f; 19 | } 20 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/common/Rewards.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.common; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | // TODO: determine format 7 | public record Rewards( 8 | @Nullable Integer rubies, 9 | @Nullable Integer experiencePoints, 10 | @Nullable Integer level, 11 | @NotNull Item[] inventory, 12 | @NotNull String[] buildplates, 13 | @NotNull Challenge[] challenges, 14 | @NotNull String[] personaItems, 15 | @NotNull UtilityBlock[] utilityBlocks 16 | ) 17 | { 18 | public record Item( 19 | @NotNull String id, 20 | int amount 21 | ) 22 | { 23 | } 24 | 25 | public record Challenge( 26 | @NotNull String id 27 | ) 28 | { 29 | } 30 | 31 | public record UtilityBlock( 32 | ) 33 | { 34 | } 35 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/buildplates/SharedBuildplate.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.buildplates; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import micheal65536.vienna.apiserver.types.inventory.Inventory; 7 | 8 | public record SharedBuildplate( 9 | @NotNull String playerId, 10 | @NotNull String sharedOn, 11 | @NotNull BuildplateData buildplateData, 12 | @NotNull Inventory inventory 13 | ) 14 | { 15 | public record BuildplateData( 16 | @NotNull Dimension dimension, 17 | @NotNull Offset offset, 18 | int blocksPerMeter, 19 | @NotNull Type type, 20 | @NotNull SurfaceOrientation surfaceOrientation, 21 | @NotNull String model, 22 | int order 23 | ) 24 | { 25 | public enum Type 26 | { 27 | @SerializedName("Survival") SURVIVAL 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/Rubies.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | public final class Rubies 4 | { 5 | public int purchased; 6 | public int earned; 7 | 8 | public Rubies() 9 | { 10 | this.purchased = 0; 11 | this.earned = 0; 12 | } 13 | 14 | public boolean spend(int amount) 15 | { 16 | if (amount > this.purchased + this.earned) 17 | { 18 | return false; 19 | } 20 | 21 | // TODO: in what order should purchased/earned rubies be spent? 22 | if (amount > this.purchased) 23 | { 24 | amount -= this.purchased; 25 | this.purchased = 0; 26 | } 27 | else 28 | { 29 | this.purchased -= amount; 30 | amount = 0; 31 | } 32 | if (amount > 0) 33 | { 34 | this.earned -= amount; 35 | } 36 | if (this.purchased < 0 || this.earned < 0) 37 | { 38 | throw new AssertionError(); 39 | } 40 | return true; 41 | } 42 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/utils/ActivityLogUtils.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.utils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.db.EarthDB; 6 | import micheal65536.vienna.db.model.player.ActivityLog; 7 | 8 | public final class ActivityLogUtils 9 | { 10 | @NotNull 11 | public static EarthDB.Query addEntry(@NotNull String playerId, @NotNull ActivityLog.Entry entry) 12 | { 13 | EarthDB.Query getQuery = new EarthDB.Query(true); 14 | getQuery.get("activityLog", playerId, ActivityLog.class); 15 | getQuery.then(results -> 16 | { 17 | ActivityLog activityLog = (ActivityLog) results.get("activityLog").value(); 18 | activityLog.addEntry(entry); 19 | activityLog.prune(); 20 | EarthDB.Query updateQuery = new EarthDB.Query(true); 21 | updateQuery.update("activityLog", playerId, activityLog); 22 | return updateQuery; 23 | }); 24 | return getQuery; 25 | } 26 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routing/Path.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routing; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Arrays; 6 | 7 | public final class Path 8 | { 9 | final String[] parts; 10 | 11 | public Path(@NotNull String path) 12 | { 13 | this.parts = Arrays.stream(path.split("/")).filter(part -> !part.isEmpty()).toArray(String[]::new); 14 | } 15 | 16 | Path(String[] parts) 17 | { 18 | this.parts = parts; 19 | } 20 | 21 | @Override 22 | @NotNull 23 | public String toString() 24 | { 25 | return String.join("/", this.parts); 26 | } 27 | 28 | public String[] getParts() 29 | { 30 | return Arrays.copyOf(this.parts, this.parts.length); 31 | } 32 | 33 | @NotNull 34 | public Path strip(int count) 35 | { 36 | if (count < 0 || count > this.parts.length) 37 | { 38 | throw new IllegalArgumentException(); 39 | } 40 | return new Path(Arrays.copyOfRange(this.parts, count, this.parts.length)); 41 | } 42 | } -------------------------------------------------------------------------------- /utils/http-routing/src/main/java/micheal65536/vienna/utils/http/routing/Path.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.utils.http.routing; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Arrays; 6 | 7 | public final class Path 8 | { 9 | final String[] parts; 10 | 11 | public Path(@NotNull String path) 12 | { 13 | this.parts = Arrays.stream(path.split("/")).filter(part -> !part.isEmpty()).toArray(String[]::new); 14 | } 15 | 16 | Path(String[] parts) 17 | { 18 | this.parts = parts; 19 | } 20 | 21 | @Override 22 | @NotNull 23 | public String toString() 24 | { 25 | return String.join("/", this.parts); 26 | } 27 | 28 | public String[] getParts() 29 | { 30 | return Arrays.copyOf(this.parts, this.parts.length); 31 | } 32 | 33 | @NotNull 34 | public Path strip(int count) 35 | { 36 | if (count < 0 || count > this.parts.length) 37 | { 38 | throw new IllegalArgumentException(); 39 | } 40 | return new Path(Arrays.copyOfRange(this.parts, count, this.parts.length)); 41 | } 42 | } -------------------------------------------------------------------------------- /eventbus/client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | eventbus-client 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.jetbrains 20 | annotations 21 | 24.1.0 22 | provided 23 | 24 | 25 | -------------------------------------------------------------------------------- /objectstore/client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | objectstore-client 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.jetbrains 20 | annotations 21 | 24.1.0 22 | provided 23 | 24 | 25 | -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/workshop/SmeltingSlot.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public final class SmeltingSlot 7 | { 8 | @Nullable 9 | public ActiveJob activeJob; 10 | @Nullable 11 | public Burning burning; 12 | public boolean locked; 13 | 14 | public SmeltingSlot() 15 | { 16 | this.activeJob = null; 17 | this.burning = null; 18 | this.locked = false; 19 | } 20 | 21 | public record ActiveJob( 22 | @NotNull String sessionId, 23 | @NotNull String recipeId, 24 | long startTime, 25 | @NotNull InputItem input, 26 | @Nullable Fuel addedFuel, 27 | int totalRounds, 28 | int collectedRounds, 29 | boolean finishedEarly 30 | ) 31 | { 32 | } 33 | 34 | public record Fuel( 35 | @NotNull InputItem item, 36 | int burnDuration, 37 | int heatPerSecond 38 | ) 39 | { 40 | } 41 | 42 | public record Burning( 43 | @NotNull Fuel fuel, 44 | int remainingHeat 45 | ) 46 | { 47 | } 48 | } -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/global/EncounterBuildplates.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.global; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | 8 | public final class EncounterBuildplates 9 | { 10 | @NotNull 11 | private final HashMap encounterBuildplates = new HashMap<>(); 12 | 13 | public EncounterBuildplates() 14 | { 15 | // empty 16 | } 17 | 18 | @Nullable 19 | public EncounterBuildplate getEncounterBuildplate(@NotNull String id) 20 | { 21 | return this.encounterBuildplates.getOrDefault(id, null); 22 | } 23 | 24 | public static final class EncounterBuildplate 25 | { 26 | public final int size; 27 | public final int offset; 28 | public final int scale; 29 | 30 | @NotNull 31 | public final String serverDataObjectId; 32 | 33 | public EncounterBuildplate(int size, int offset, int scale, @NotNull String serverDataObjectId) 34 | { 35 | this.size = size; 36 | this.offset = offset; 37 | this.scale = scale; 38 | 39 | this.serverDataObjectId = serverDataObjectId; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /staticdata/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | staticdata 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | com.google.code.gson 20 | gson 21 | 2.10.1 22 | 23 | 24 | 25 | org.jetbrains 26 | annotations 27 | 24.1.0 28 | provided 29 | 30 | 31 | -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/workshop/SmeltingSlot.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.workshop; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import micheal65536.vienna.apiserver.types.common.BurnRate; 7 | 8 | public record SmeltingSlot( 9 | @Nullable Fuel fuel, 10 | @Nullable Burning burning, 11 | @Nullable String sessionId, 12 | @Nullable String recipeId, 13 | @Nullable OutputItem output, 14 | InputItem[] escrow, 15 | int completed, 16 | int available, 17 | int total, 18 | @Nullable String nextCompletionUtc, 19 | @Nullable String totalCompletionUtc, 20 | @NotNull State state, 21 | @Nullable BoostState boostState, 22 | @Nullable UnlockPrice unlockPrice, 23 | int streamVersion 24 | ) 25 | { 26 | public record Fuel( 27 | @NotNull BurnRate burnRate, 28 | @NotNull String itemId, 29 | int quantity, 30 | String[] itemInstanceIds 31 | ) 32 | { 33 | } 34 | 35 | public record Burning( 36 | @Nullable String burnStartTime, 37 | @Nullable String burnsUntil, 38 | @Nullable String remainingBurnTime, 39 | @Nullable Float heatDepleted, 40 | @NotNull Fuel fuel 41 | ) 42 | { 43 | } 44 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | vienna 9 | 0.0.1-SNAPSHOT 10 | pom 11 | 12 | apiserver 13 | buildplate/launcher 14 | buildplate/connector-plugin 15 | buildplate/connector-model 16 | tappablesgenerator 17 | db 18 | staticdata 19 | eventbus/server 20 | eventbus/client 21 | objectstore/server 22 | objectstore/client 23 | utils/http-routing 24 | utils/locator 25 | utils/cdn 26 | utils/tools/buildplate-importer 27 | 28 | -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routing/Application.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routing; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import org.apache.logging.log4j.LogManager; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.io.IOException; 9 | 10 | public final class Application 11 | { 12 | public final Router router = new Router(); 13 | 14 | public Application() 15 | { 16 | // empty 17 | } 18 | 19 | public void handleRequest(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse) 20 | { 21 | LogManager.getLogger().info("Request: {} {}", httpServletRequest.getMethod(), httpServletRequest.getRequestURI() + (httpServletRequest.getQueryString() != null ? "?" + httpServletRequest.getQueryString() : "")); 22 | if (!this.router.handleRequest(httpServletRequest, httpServletResponse)) 23 | { 24 | httpServletResponse.setStatus(404); 25 | try 26 | { 27 | httpServletResponse.getOutputStream().close(); 28 | } 29 | catch (IOException exception) 30 | { 31 | LogManager.getLogger().warn("IOException while sending response for unmatched request", exception); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/journal/Journal.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.journal; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import micheal65536.vienna.apiserver.types.common.Rewards; 7 | 8 | import java.util.HashMap; 9 | 10 | public record Journal( 11 | @NotNull HashMap inventoryJournal, 12 | @NotNull ActivityLogEntry[] activityLog 13 | ) 14 | { 15 | public record InventoryJournalEntry( 16 | @NotNull String firstSeen, 17 | @NotNull String lastSeen, 18 | int amountCollected 19 | ) 20 | { 21 | } 22 | 23 | public record ActivityLogEntry( 24 | @NotNull Type scenario, 25 | @NotNull String eventTime, 26 | @NotNull Rewards rewards, 27 | @NotNull HashMap properties 28 | ) 29 | { 30 | public enum Type 31 | { 32 | @SerializedName("LevelUp") LEVEL_UP, 33 | @SerializedName("TappableCollected") TAPPABLE, 34 | @SerializedName("JournalContentCollected") JOURNAL_ITEM_UNLOCKED, 35 | @SerializedName("CraftingJobCompleted") CRAFTING_COMPLETED, 36 | @SerializedName("SmeltingJobCompleted") SMELTING_COMPLETED, 37 | @SerializedName("BoostActivated") BOOST_ACTIVATED 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /utils/http-routing/src/main/java/micheal65536/vienna/utils/http/routing/Application.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.utils.http.routing; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import org.apache.logging.log4j.LogManager; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.io.IOException; 9 | 10 | public final class Application 11 | { 12 | public final Router router = new Router(); 13 | 14 | public Application() 15 | { 16 | // empty 17 | } 18 | 19 | public void handleRequest(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse) 20 | { 21 | LogManager.getLogger().info("Request: {} {}", httpServletRequest.getMethod(), httpServletRequest.getRequestURI() + (httpServletRequest.getQueryString() != null ? "?" + httpServletRequest.getQueryString() : "")); 22 | if (!this.router.handleRequest(httpServletRequest, httpServletResponse)) 23 | { 24 | httpServletResponse.setStatus(404); 25 | try 26 | { 27 | httpServletResponse.getOutputStream().close(); 28 | } 29 | catch (IOException exception) 30 | { 31 | LogManager.getLogger().warn("IOException while sending response for unmatched request", exception); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routes/ResourcePacksRouter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routes; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.apiserver.routing.Request; 6 | import micheal65536.vienna.apiserver.routing.Response; 7 | import micheal65536.vienna.apiserver.routing.Router; 8 | import micheal65536.vienna.apiserver.utils.EarthApiResponse; 9 | 10 | public class ResourcePacksRouter extends Router 11 | { 12 | public ResourcePacksRouter() 13 | { 14 | // TODO: make this configurable 15 | record ResourcePack( 16 | int order, 17 | @NotNull String resourcePackId, 18 | @NotNull String resourcePackVersion, 19 | int[] parsedResourcePackVersion, 20 | @NotNull String relativePath 21 | ) 22 | { 23 | } 24 | this.addHandler(new Route.Builder(Request.Method.GET, "/resourcepacks/$buildNumber/default").build(), request -> Response.okFromJson(new EarthApiResponse<>(new ResourcePack[]{ 25 | new ResourcePack( 26 | 0, 27 | "dba38e59-091a-4826-b76a-a08d7de5a9e2", 28 | "2020.1214.04", 29 | new int[]{2020, 1214, 4}, 30 | "availableresourcepack/resourcepacks/dba38e59-091a-4826-b76a-a08d7de5a9e2-1301b0c257a311678123b9e7325d0d6c61db3c35" 31 | ) 32 | }), EarthApiResponse.class)); 33 | } 34 | } -------------------------------------------------------------------------------- /staticdata/src/main/java/micheal65536/vienna/staticdata/EncountersConfig.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.staticdata; 2 | 3 | import com.google.gson.Gson; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.io.File; 7 | import java.io.FileReader; 8 | import java.util.LinkedList; 9 | 10 | public final class EncountersConfig 11 | { 12 | @NotNull 13 | public final EncounterConfig[] encounters; 14 | 15 | EncountersConfig(@NotNull File dir) throws StaticDataException 16 | { 17 | try 18 | { 19 | LinkedList encounters = new LinkedList<>(); 20 | for (File file : dir.listFiles()) 21 | { 22 | if (file.isFile() && file.getName().endsWith(".json")) 23 | { 24 | encounters.add(new Gson().fromJson(new FileReader(file), EncounterConfig.class)); 25 | } 26 | } 27 | this.encounters = encounters.toArray(EncounterConfig[]::new); 28 | } 29 | catch (Exception exception) 30 | { 31 | throw new StaticDataException(exception); 32 | } 33 | } 34 | 35 | public record EncounterConfig( 36 | @NotNull String icon, 37 | @NotNull Rarity rarity, 38 | @NotNull String encounterBuildplateId, 39 | int duration 40 | ) 41 | { 42 | public enum Rarity 43 | { 44 | COMMON, 45 | UNCOMMON, 46 | RARE, 47 | EPIC, 48 | LEGENDARY 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/utils/TimeFormatter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.utils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.sql.Date; 6 | import java.text.SimpleDateFormat; 7 | import java.time.Instant; 8 | import java.util.Locale; 9 | import java.util.TimeZone; 10 | 11 | public final class TimeFormatter 12 | { 13 | private static final SimpleDateFormat JSON_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT); 14 | private static final String JSON_DURATION_FORMAT = "%d:%02d:%02d"; 15 | 16 | static 17 | { 18 | JSON_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); 19 | } 20 | 21 | @NotNull 22 | public static String formatTime(long time) 23 | { 24 | return JSON_DATE_FORMAT.format(Date.from(Instant.ofEpochMilli(time))); 25 | } 26 | 27 | @NotNull 28 | public static String formatDuration(long duration) 29 | { 30 | return JSON_DURATION_FORMAT.formatted(duration / 3600000, (duration % 3600000) / 60000, (duration % 60000) / 1000); 31 | } 32 | 33 | public static long parseDuration(@NotNull String duration) 34 | { 35 | long duration1 = 0; 36 | String[] parts = duration.split(":", 3); 37 | duration1 = ((Long.parseLong(parts[0]) * 60 + Long.parseLong(parts[1])) * 60 + Long.parseLong(parts[2])) * 1000; 38 | return duration1; 39 | } 40 | } -------------------------------------------------------------------------------- /db/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | db 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.xerial 20 | sqlite-jdbc 21 | 3.45.1.0 22 | 23 | 24 | com.google.code.gson 25 | gson 26 | 2.10.1 27 | 28 | 29 | 30 | org.jetbrains 31 | annotations 32 | 24.1.0 33 | provided 34 | 35 | 36 | -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/Boosts.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.Arrays; 7 | import java.util.LinkedList; 8 | 9 | public final class Boosts 10 | { 11 | @Nullable 12 | public final ActiveBoost[] activeBoosts; 13 | 14 | public Boosts() 15 | { 16 | this.activeBoosts = new ActiveBoost[5]; 17 | } 18 | 19 | @Nullable 20 | public ActiveBoost get(@NotNull String instanceId) 21 | { 22 | return Arrays.stream(this.activeBoosts).filter(activeBoost -> activeBoost != null && activeBoost.instanceId.equals(instanceId)).findFirst().orElse(null); 23 | } 24 | 25 | @NotNull 26 | public ActiveBoost[] prune(long currentTime) 27 | { 28 | LinkedList prunedBoosts = new LinkedList<>(); 29 | for (int index = 0; index < this.activeBoosts.length; index++) 30 | { 31 | ActiveBoost activeBoost = this.activeBoosts[index]; 32 | if (activeBoost != null && activeBoost.startTime + activeBoost.duration < currentTime) 33 | { 34 | this.activeBoosts[index] = null; 35 | prunedBoosts.add(activeBoost); 36 | } 37 | } 38 | return prunedBoosts.toArray(ActiveBoost[]::new); 39 | } 40 | 41 | public record ActiveBoost( 42 | @NotNull String instanceId, 43 | @NotNull String itemId, 44 | long startTime, 45 | long duration 46 | ) 47 | { 48 | } 49 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/catalog/RecipesCatalog.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.catalog; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public record RecipesCatalog( 6 | @NotNull CraftingRecipe[] crafting, 7 | @NotNull SmeltingRecipe[] smelting 8 | ) 9 | { 10 | public record CraftingRecipe( 11 | @NotNull String id, 12 | @NotNull String category, 13 | @NotNull String duration, 14 | @NotNull Ingredient[] ingredients, 15 | @NotNull Output output, 16 | @NotNull ReturnItem[] returnItems, 17 | boolean deprecated 18 | ) 19 | { 20 | public record Ingredient( 21 | @NotNull String[] items, 22 | int quantity 23 | ) 24 | { 25 | } 26 | 27 | public record Output( 28 | @NotNull String itemId, 29 | int quantity 30 | ) 31 | { 32 | } 33 | 34 | public record ReturnItem( 35 | @NotNull String id, 36 | int amount 37 | ) 38 | { 39 | } 40 | } 41 | 42 | public record SmeltingRecipe( 43 | @NotNull String id, 44 | int heatRequired, 45 | @NotNull String inputItemId, 46 | @NotNull Output output, 47 | @NotNull ReturnItem[] returnItems, 48 | boolean deprecated 49 | ) 50 | { 51 | public record Output( 52 | @NotNull String itemId, 53 | int quantity 54 | ) 55 | { 56 | } 57 | 58 | public record ReturnItem( 59 | @NotNull String id, 60 | int amount 61 | ) 62 | { 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /objectstore/server/src/main/java/micheal65536/vienna/objectstore/server/Server.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.objectstore.server; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | public class Server 8 | { 9 | private final DataStore dataStore; 10 | 11 | public Server(@NotNull DataStore dataStore) 12 | { 13 | this.dataStore = dataStore; 14 | } 15 | 16 | @Nullable 17 | public String store(byte[] data) 18 | { 19 | try 20 | { 21 | String id = this.dataStore.store(data); 22 | LogManager.getLogger().info("Stored new object {}", id); 23 | return id; 24 | } 25 | catch (DataStore.DataStoreException exception) 26 | { 27 | LogManager.getLogger().error("Could not store object", exception); 28 | return null; 29 | } 30 | } 31 | 32 | public byte[] load(@NotNull String id) 33 | { 34 | LogManager.getLogger().info("Request for object {}", id); 35 | try 36 | { 37 | byte[] data = this.dataStore.load(id); 38 | if (data == null) 39 | { 40 | LogManager.getLogger().info("Requested object {} does not exist", id); 41 | } 42 | return data; 43 | } 44 | catch (DataStore.DataStoreException exception) 45 | { 46 | LogManager.getLogger().error("Could not load object {}", id, exception); 47 | return null; 48 | } 49 | } 50 | 51 | public boolean delete(@NotNull String id) 52 | { 53 | LogManager.getLogger().info("Request to delete object {}", id); 54 | this.dataStore.delete(id); 55 | return true; 56 | } 57 | } -------------------------------------------------------------------------------- /eventbus/server/src/main/java/micheal65536/vienna/eventbus/server/Main.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.eventbus.server; 2 | 3 | import org.apache.commons.cli.CommandLine; 4 | import org.apache.commons.cli.DefaultParser; 5 | import org.apache.commons.cli.Option; 6 | import org.apache.commons.cli.Options; 7 | import org.apache.commons.cli.ParseException; 8 | import org.apache.logging.log4j.Level; 9 | import org.apache.logging.log4j.LogManager; 10 | import org.apache.logging.log4j.core.config.Configurator; 11 | 12 | import java.io.IOException; 13 | 14 | public class Main 15 | { 16 | public static void main(String[] args) 17 | { 18 | Configurator.setRootLevel(Level.DEBUG); 19 | 20 | Options options = new Options(); 21 | options.addOption(Option.builder() 22 | .option("port") 23 | .hasArg() 24 | .argName("port") 25 | .desc("Port to listen on, defaults to 5532") 26 | .build()); 27 | CommandLine commandLine; 28 | int port; 29 | try 30 | { 31 | commandLine = new DefaultParser().parse(options, args); 32 | port = commandLine.hasOption("port") ? (int) commandLine.getParsedOptionValue("port") : 5532; 33 | } 34 | catch (ParseException exception) 35 | { 36 | LogManager.getLogger().fatal(exception); 37 | System.exit(1); 38 | return; 39 | } 40 | 41 | NetworkServer server; 42 | try 43 | { 44 | server = new NetworkServer(new Server(), port); 45 | } 46 | catch (IOException exception) 47 | { 48 | LogManager.getLogger().fatal(exception); 49 | System.exit(1); 50 | return; 51 | } 52 | server.run(); 53 | } 54 | } -------------------------------------------------------------------------------- /staticdata/src/main/java/micheal65536/vienna/staticdata/Levels.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.staticdata; 2 | 3 | import com.google.gson.Gson; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.io.File; 7 | import java.io.FileReader; 8 | import java.util.LinkedList; 9 | 10 | public final class Levels 11 | { 12 | @NotNull 13 | public final Level[] levels; 14 | 15 | Levels(@NotNull File dir) throws StaticDataException 16 | { 17 | try 18 | { 19 | LinkedList levels = new LinkedList<>(); 20 | File file; 21 | for (int level = 2; (file = new File(dir, Integer.toString(level) + ".json")).isFile(); level++) 22 | { 23 | levels.add(new Gson().fromJson(new FileReader(file), Level.class)); 24 | } 25 | this.levels = levels.toArray(Level[]::new); 26 | 27 | for (int index = 1; index < this.levels.length; index++) 28 | { 29 | if (this.levels[index].experienceRequired <= this.levels[index - 1].experienceRequired) 30 | { 31 | throw new StaticDataException("Level %d has lower experience required than preceding level %d".formatted(index + 2, index + 1)); 32 | } 33 | } 34 | } 35 | catch (StaticDataException exception) 36 | { 37 | throw exception; 38 | } 39 | catch (Exception exception) 40 | { 41 | throw new StaticDataException(exception); 42 | } 43 | } 44 | 45 | public record Level( 46 | int experienceRequired, 47 | int rubies, 48 | @NotNull Item[] items, 49 | @NotNull String[] buildplates 50 | ) 51 | { 52 | public record Item( 53 | @NotNull String id, 54 | int count 55 | ) 56 | { 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/utils/EarthApiResponse.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.utils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.db.EarthDB; 6 | 7 | import java.util.HashMap; 8 | 9 | public class EarthApiResponse 10 | { 11 | private final T result; 12 | private final HashMap updates = new HashMap<>(); 13 | 14 | public EarthApiResponse(T result) 15 | { 16 | this.result = result; 17 | } 18 | 19 | public EarthApiResponse(T result, @NotNull Updates updates) 20 | { 21 | this.result = result; 22 | this.updates.putAll(updates.map); 23 | } 24 | 25 | public static final class Updates 26 | { 27 | private final HashMap map = new HashMap<>(); 28 | 29 | public Updates(@NotNull EarthDB.Results results) 30 | { 31 | HashMap updates = results.getUpdates(); 32 | this.put(updates, "profile", "characterProfile"); 33 | this.put(updates, "inventory", "inventory"); 34 | this.put(updates, "crafting", "crafting"); 35 | this.put(updates, "smelting", "smelting"); 36 | this.put(updates, "boosts", "boosts"); 37 | this.put(updates, "buildplates", "buildplates"); 38 | this.put(updates, "journal", "playerJournal"); 39 | this.put(updates, "challenges", "challenges"); 40 | this.put(updates, "tokens", "tokens"); 41 | } 42 | 43 | private void put(@NotNull HashMap updates, @NotNull String name, @NotNull String as) 44 | { 45 | Integer version = updates.getOrDefault(name, null); 46 | if (version != null) 47 | { 48 | this.map.put(as, version); 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/Journal.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | 8 | public final class Journal 9 | { 10 | @NotNull 11 | private final HashMap items; 12 | 13 | public Journal() 14 | { 15 | this.items = new HashMap<>(); 16 | } 17 | 18 | @NotNull 19 | public Journal copy() 20 | { 21 | Journal journal = new Journal(); 22 | journal.items.putAll(this.items); 23 | return journal; 24 | } 25 | 26 | @NotNull 27 | public HashMap getItems() 28 | { 29 | return new HashMap<>(this.items); 30 | } 31 | 32 | @Nullable 33 | public ItemJournalEntry getItem(@NotNull String uuid) 34 | { 35 | return this.items.getOrDefault(uuid, null); 36 | } 37 | 38 | public int addCollectedItem(@NotNull String uuid, long timestamp, int count) 39 | { 40 | if (count < 0) 41 | { 42 | throw new IllegalArgumentException(); 43 | } 44 | ItemJournalEntry itemJournalEntry = this.items.getOrDefault(uuid, null); 45 | if (itemJournalEntry == null) 46 | { 47 | this.items.put(uuid, new ItemJournalEntry(timestamp, timestamp, count)); 48 | return 0; 49 | } 50 | else 51 | { 52 | this.items.put(uuid, new ItemJournalEntry(itemJournalEntry.firstSeen, itemJournalEntry.lastSeen, itemJournalEntry.amountCollected + count)); 53 | return itemJournalEntry.amountCollected; 54 | } 55 | } 56 | 57 | public record ItemJournalEntry( 58 | long firstSeen, 59 | long lastSeen, 60 | int amountCollected 61 | ) 62 | { 63 | } 64 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/boosts/Boosts.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.boosts; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import micheal65536.vienna.apiserver.types.common.Effect; 7 | 8 | import java.util.HashMap; 9 | 10 | public record Boosts( 11 | Potion[] potions, 12 | MiniFig[] miniFigs, 13 | @NotNull ActiveEffect[] activeEffects, 14 | @NotNull HashMap scenarioBoosts, 15 | @NotNull StatusEffects statusEffects, 16 | @NotNull HashMap miniFigRecords, 17 | @Nullable String expiration 18 | ) 19 | { 20 | public record Potion( 21 | boolean enabled, 22 | @NotNull String itemId, 23 | @NotNull String instanceId, 24 | @NotNull String expiration 25 | ) 26 | { 27 | } 28 | 29 | public record MiniFig( 30 | // TODO 31 | ) 32 | { 33 | } 34 | 35 | public record ActiveEffect( 36 | @NotNull Effect effect, 37 | @NotNull String expiration 38 | ) 39 | { 40 | } 41 | 42 | public record ScenarioBoost( 43 | boolean enabled, 44 | @NotNull String instanceId, 45 | @NotNull Effect[] effects, 46 | @NotNull String expiration 47 | ) 48 | { 49 | } 50 | 51 | public record StatusEffects( 52 | @Nullable Integer tappableInteractionRadius, 53 | @Nullable Integer experiencePointRate, 54 | @Nullable Integer itemExperiencePointRates, 55 | @Nullable Integer attackDamageRate, 56 | @Nullable Integer playerDefenseRate, 57 | @Nullable Integer blockDamageRate, 58 | @Nullable Integer maximumPlayerHealth, 59 | @Nullable Integer craftingSpeed, 60 | @Nullable Integer smeltingFuelIntensity, 61 | @Nullable Float foodHealthRate 62 | ) 63 | { 64 | } 65 | 66 | public record MiniFigRecord( 67 | // TODO 68 | ) 69 | { 70 | } 71 | } -------------------------------------------------------------------------------- /utils/http-routing/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | utils-http-routing 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.apache.tomcat.embed 20 | tomcat-embed-core 21 | 10.1.19 22 | 23 | 24 | 25 | com.google.code.gson 26 | gson 27 | 2.10.1 28 | 29 | 30 | 31 | org.apache.logging.log4j 32 | log4j-api 33 | 2.22.1 34 | 35 | 36 | org.apache.logging.log4j 37 | log4j-core 38 | 2.22.1 39 | 40 | 41 | 42 | org.jetbrains 43 | annotations 44 | 24.1.0 45 | provided 46 | 47 | 48 | -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/Buildplates.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | 8 | public final class Buildplates 9 | { 10 | @NotNull 11 | private final HashMap buildplates = new HashMap<>(); 12 | 13 | public Buildplates() 14 | { 15 | // empty 16 | } 17 | 18 | public void addBuildplate(@NotNull String id, @NotNull Buildplate buildplate) 19 | { 20 | this.buildplates.put(id, buildplate); 21 | } 22 | 23 | @Nullable 24 | public Buildplate getBuildplate(@NotNull String id) 25 | { 26 | return this.buildplates.getOrDefault(id, null); 27 | } 28 | 29 | public record BuildplateEntry( 30 | @NotNull String id, 31 | @NotNull Buildplate buildplate 32 | ) 33 | { 34 | } 35 | 36 | @NotNull 37 | public BuildplateEntry[] getBuildplates() 38 | { 39 | return this.buildplates.entrySet().stream().map(entry -> new BuildplateEntry(entry.getKey(), entry.getValue())).toArray(BuildplateEntry[]::new); 40 | } 41 | 42 | public static final class Buildplate 43 | { 44 | public final int size; 45 | public final int offset; 46 | public final int scale; 47 | 48 | public final boolean night; 49 | 50 | public long lastModified; 51 | @NotNull 52 | public String serverDataObjectId; 53 | @NotNull 54 | public String previewObjectId; 55 | 56 | public Buildplate(int size, int offset, int scale, boolean night, long lastModified, @NotNull String serverDataObjectId, @NotNull String previewObjectId) 57 | { 58 | this.size = size; 59 | this.offset = offset; 60 | this.scale = scale; 61 | 62 | this.night = night; 63 | 64 | this.lastModified = lastModified; 65 | this.serverDataObjectId = serverDataObjectId; 66 | this.previewObjectId = previewObjectId; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routes/SigninRouter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routes; 2 | 3 | import micheal65536.vienna.apiserver.routing.Request; 4 | import micheal65536.vienna.apiserver.routing.Response; 5 | import micheal65536.vienna.apiserver.routing.Router; 6 | import micheal65536.vienna.apiserver.utils.EarthApiResponse; 7 | import micheal65536.vienna.apiserver.utils.MapBuilder; 8 | 9 | import java.util.HashMap; 10 | import java.util.Locale; 11 | 12 | public final class SigninRouter extends Router 13 | { 14 | public SigninRouter() 15 | { 16 | this.addHandler( 17 | new Route.Builder(Request.Method.POST, "/player/profile/signin") 18 | .addHeaderParameter("sessionId", "Session-Id") 19 | .build(), 20 | request -> 21 | { 22 | record SigninRequest(String sessionTicket) 23 | { 24 | } 25 | SigninRequest signinRequest = request.getBodyAsJson(SigninRequest.class); 26 | 27 | String[] parts = signinRequest.sessionTicket.split("-", 2); 28 | if (parts.length != 2) 29 | { 30 | return Response.badRequest(); 31 | } 32 | 33 | String userId = parts[0]; 34 | if (!userId.matches("^[0-9A-F]{16}$")) 35 | { 36 | return Response.badRequest(); 37 | } 38 | 39 | // TODO: check credentials 40 | 41 | // TODO: generate secure session token 42 | String token = userId.toLowerCase(Locale.ROOT); 43 | 44 | return Response.okFromJson(new EarthApiResponse<>(new MapBuilder<>() 45 | .put("basePath", "/auth") 46 | .put("authenticationToken", token) 47 | .put("clientProperties", new HashMap<>()) 48 | .put("mixedReality", null) 49 | .put("mrToken", null) 50 | .put("streams", null) 51 | .put("tokens", new HashMap<>()) 52 | .put("updates", new HashMap<>()) 53 | .getMap()), EarthApiResponse.class); 54 | } 55 | ); 56 | } 57 | } -------------------------------------------------------------------------------- /staticdata/src/main/java/micheal65536/vienna/staticdata/TappablesConfig.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.staticdata; 2 | 3 | import com.google.gson.Gson; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.io.File; 7 | import java.io.FileReader; 8 | import java.util.HashMap; 9 | import java.util.LinkedList; 10 | 11 | public final class TappablesConfig 12 | { 13 | @NotNull 14 | public final TappableConfig[] tappables; 15 | 16 | TappablesConfig(@NotNull File dir) throws StaticDataException 17 | { 18 | try 19 | { 20 | LinkedList tappables = new LinkedList<>(); 21 | for (File file : dir.listFiles()) 22 | { 23 | if (file.isFile() && file.getName().endsWith(".json")) 24 | { 25 | tappables.add(new Gson().fromJson(new FileReader(file), TappableConfig.class)); 26 | } 27 | } 28 | this.tappables = tappables.toArray(TappableConfig[]::new); 29 | 30 | for (TappableConfig tappableConfig : this.tappables) 31 | { 32 | for (TappableConfig.DropSet dropSet : tappableConfig.dropSets) 33 | { 34 | for (String itemId : dropSet.items) 35 | { 36 | if (!tappableConfig.itemCounts.containsKey(itemId)) 37 | { 38 | throw new StaticDataException("Tappable config %s has no item count for item %s".formatted(tappableConfig.icon, itemId)); 39 | } 40 | } 41 | } 42 | } 43 | } 44 | catch (StaticDataException exception) 45 | { 46 | throw exception; 47 | } 48 | catch (Exception exception) 49 | { 50 | throw new StaticDataException(exception); 51 | } 52 | } 53 | 54 | public record TappableConfig( 55 | @NotNull String icon, 56 | @NotNull DropSet[] dropSets, 57 | @NotNull HashMap itemCounts 58 | ) 59 | { 60 | public record DropSet( 61 | @NotNull String[] items, 62 | int chance 63 | ) 64 | { 65 | } 66 | 67 | public record ItemCount( 68 | int min, 69 | int max 70 | ) 71 | { 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /objectstore/server/src/main/java/micheal65536/vienna/objectstore/server/Main.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.objectstore.server; 2 | 3 | import org.apache.commons.cli.CommandLine; 4 | import org.apache.commons.cli.DefaultParser; 5 | import org.apache.commons.cli.Option; 6 | import org.apache.commons.cli.Options; 7 | import org.apache.commons.cli.ParseException; 8 | import org.apache.logging.log4j.Level; 9 | import org.apache.logging.log4j.LogManager; 10 | import org.apache.logging.log4j.core.config.Configurator; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | 15 | public class Main 16 | { 17 | public static void main(String[] args) 18 | { 19 | Configurator.setRootLevel(Level.DEBUG); 20 | 21 | Options options = new Options(); 22 | options.addOption(Option.builder() 23 | .option("dataDir") 24 | .hasArg() 25 | .argName("dir") 26 | .required() 27 | .desc("Directory where data is stored") 28 | .build()); 29 | options.addOption(Option.builder() 30 | .option("port") 31 | .hasArg() 32 | .argName("port") 33 | .type(Number.class) 34 | .desc("Port to listen on, defaults to 5396") 35 | .build()); 36 | CommandLine commandLine; 37 | String dataDir; 38 | int port; 39 | try 40 | { 41 | commandLine = new DefaultParser().parse(options, args); 42 | dataDir = commandLine.getOptionValue("dataDir"); 43 | port = commandLine.hasOption("port") ? (int) (long) commandLine.getParsedOptionValue("port") : 5396; 44 | } 45 | catch (ParseException exception) 46 | { 47 | LogManager.getLogger().fatal(exception); 48 | System.exit(1); 49 | return; 50 | } 51 | 52 | NetworkServer server; 53 | try 54 | { 55 | server = new NetworkServer(new Server(new DataStore(new File(dataDir))), port); 56 | } 57 | catch (IOException | DataStore.DataStoreException exception) 58 | { 59 | LogManager.getLogger().fatal(exception); 60 | System.exit(1); 61 | return; 62 | } 63 | server.run(); 64 | } 65 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/Hotbar.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | import java.util.HashSet; 8 | 9 | public final class Hotbar 10 | { 11 | public final Item[] items; 12 | 13 | public Hotbar() 14 | { 15 | this.items = new Item[7]; 16 | } 17 | 18 | public void limitToInventory(@NotNull Inventory inventory) 19 | { 20 | HashMap usedStackableItemCounts = new HashMap<>(); 21 | HashMap> usedNonStackableItemInstances = new HashMap<>(); 22 | for (int index = 0; index < this.items.length; index++) 23 | { 24 | Item item = this.items[index]; 25 | if (item == null) 26 | { 27 | continue; 28 | } 29 | if (item.instanceId() != null) 30 | { 31 | if (inventory.getItemInstance(item.uuid(), item.instanceId()) != null) 32 | { 33 | HashSet usedItemInstances = usedNonStackableItemInstances.computeIfAbsent(item.uuid(), uuid -> new HashSet<>()); 34 | if (!usedItemInstances.add(item.instanceId())) 35 | { 36 | item = null; 37 | } 38 | } 39 | else 40 | { 41 | item = null; 42 | } 43 | } 44 | else 45 | { 46 | int inventoryCount = inventory.getItemCount(item.uuid()); 47 | int usedCount = usedStackableItemCounts.getOrDefault(item.uuid(), 0); 48 | if (inventoryCount - usedCount > 0) 49 | { 50 | if (inventoryCount - usedCount < item.count()) 51 | { 52 | item = new Item(item.uuid(), inventoryCount - usedCount, null); 53 | } 54 | usedCount += item.count(); 55 | usedStackableItemCounts.put(item.uuid(), usedCount); 56 | } 57 | else 58 | { 59 | item = null; 60 | } 61 | } 62 | this.items[index] = item; 63 | } 64 | } 65 | 66 | public record Item( 67 | @NotNull String uuid, 68 | @NotNull int count, 69 | @Nullable String instanceId 70 | ) 71 | { 72 | } 73 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/utils/LevelUtils.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.utils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.db.EarthDB; 6 | import micheal65536.vienna.db.model.player.Profile; 7 | import micheal65536.vienna.db.model.player.Tokens; 8 | import micheal65536.vienna.staticdata.Levels; 9 | import micheal65536.vienna.staticdata.StaticData; 10 | 11 | public final class LevelUtils 12 | { 13 | @NotNull 14 | public static EarthDB.Query checkAndHandlePlayerLevelUp(@NotNull String playerId, long currentTime, @NotNull StaticData staticData) 15 | { 16 | EarthDB.Query getQuery = new EarthDB.Query(true); 17 | getQuery.get("profile", playerId, Profile.class); 18 | getQuery.then(results -> 19 | { 20 | Profile profile = (Profile) results.get("profile").value(); 21 | EarthDB.Query updateQuery = new EarthDB.Query(true); 22 | boolean changed = false; 23 | while (profile.level - 1 < staticData.levels.levels.length && profile.experience >= staticData.levels.levels[profile.level - 1].experienceRequired()) 24 | { 25 | changed = true; 26 | profile.level++; 27 | Rewards rewards = makeLevelRewards(staticData.levels.levels[profile.level - 2]); 28 | updateQuery.then(TokenUtils.addToken(playerId, new Tokens.LevelUpToken(profile.level, rewards.toDBRewardsModel())), false); 29 | } 30 | if (changed) 31 | { 32 | updateQuery.update("profile", playerId, profile); 33 | } 34 | return updateQuery; 35 | }); 36 | return getQuery; 37 | } 38 | 39 | @NotNull 40 | public static Rewards makeLevelRewards(@NotNull Levels.Level level) 41 | { 42 | Rewards rewards = new Rewards(); 43 | if (level.rubies() > 0) 44 | { 45 | rewards.addRubies(level.rubies()); 46 | } 47 | for (Levels.Level.Item item : level.items()) 48 | { 49 | rewards.addItem(item.id(), item.count()); 50 | } 51 | for (String buildplate : level.buildplates()) 52 | { 53 | rewards.addBuildplate(buildplate); 54 | } 55 | return rewards; 56 | } 57 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/global/SharedBuildplates.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.global; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | 8 | public final class SharedBuildplates 9 | { 10 | @NotNull 11 | private final HashMap sharedBuildplates = new HashMap<>(); 12 | 13 | public SharedBuildplates() 14 | { 15 | // empty 16 | } 17 | 18 | public void addSharedBuildplate(@NotNull String id, @NotNull SharedBuildplate buildplate) 19 | { 20 | this.sharedBuildplates.put(id, buildplate); 21 | } 22 | 23 | @Nullable 24 | public SharedBuildplate getSharedBuildplate(@NotNull String id) 25 | { 26 | return this.sharedBuildplates.getOrDefault(id, null); 27 | } 28 | 29 | public static final class SharedBuildplate 30 | { 31 | @NotNull 32 | public final String playerId; 33 | 34 | public final int size; 35 | public final int offset; 36 | public final int scale; 37 | 38 | public final boolean night; 39 | 40 | public final long created; 41 | public final long buildplateLastModifed; 42 | public long lastViewed; 43 | public int numberOfTimesViewed; 44 | 45 | public final HotbarItem[] hotbar; 46 | 47 | @NotNull 48 | public final String serverDataObjectId; 49 | 50 | public SharedBuildplate(@NotNull String playerId, int size, int offset, int scale, boolean night, long created, long buildplateLastModifed, @NotNull String serverDataObjectId) 51 | { 52 | this.playerId = playerId; 53 | 54 | this.size = size; 55 | this.offset = offset; 56 | this.scale = scale; 57 | 58 | this.night = night; 59 | 60 | this.created = created; 61 | this.buildplateLastModifed = buildplateLastModifed; 62 | this.lastViewed = 0; 63 | this.numberOfTimesViewed = 0; 64 | 65 | this.hotbar = new HotbarItem[7]; 66 | 67 | this.serverDataObjectId = serverDataObjectId; 68 | } 69 | 70 | public record HotbarItem( 71 | @NotNull String uuid, 72 | int count, 73 | @Nullable String instanceId, 74 | int wear 75 | ) 76 | { 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/buildplates/BuildplateInstance.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.buildplates; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import micheal65536.vienna.apiserver.types.common.Coordinate; 8 | import micheal65536.vienna.apiserver.types.common.Rarity; 9 | 10 | import java.util.HashMap; 11 | 12 | public record BuildplateInstance( 13 | @NotNull String instanceId, 14 | @NotNull String partitionId, 15 | @NotNull String fqdn, 16 | @NotNull String ipV4Address, 17 | int port, 18 | boolean serverReady, 19 | @NotNull ApplicationStatus applicationStatus, 20 | @NotNull ServerStatus serverStatus, 21 | @NotNull String metadata, 22 | @NotNull GameplayMetadata gameplayMetadata, 23 | @NotNull String roleInstance, // TODO: find out what this is 24 | @NotNull Coordinate hostCoordinate 25 | ) 26 | { 27 | public enum ApplicationStatus 28 | { 29 | @SerializedName("Unknown") UNKNOWN, 30 | @SerializedName("Ready") READY 31 | } 32 | 33 | public enum ServerStatus 34 | { 35 | @SerializedName("Running") RUNNING 36 | } 37 | 38 | public record GameplayMetadata( 39 | @NotNull String worldId, 40 | @NotNull String templateId, 41 | @Nullable String spawningPlayerId, 42 | @NotNull String spawningClientBuildNumber, 43 | @NotNull String playerJoinCode, 44 | @NotNull Dimension dimension, 45 | @NotNull Offset offset, 46 | int blocksPerMeter, 47 | boolean isFullSize, 48 | @NotNull GameplayMode gameplayMode, 49 | @NotNull SurfaceOrientation surfaceOrientation, 50 | @Nullable String augmentedImageSetId, 51 | @Nullable Rarity rarity, 52 | @NotNull HashMap breakableItemToItemLootMap // TODO: find out what this is 53 | ) 54 | { 55 | public enum GameplayMode 56 | { 57 | @SerializedName("Buildplate") BUILDPLATE, 58 | @SerializedName("BuildplatePlay") BUILDPLATE_PLAY, 59 | @SerializedName("SharedBuildplatePlay") SHARED_BUILDPLATE_PLAY, 60 | @SerializedName("Encounter") ENCOUNTER 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /eventbus/client/src/main/java/micheal65536/vienna/eventbus/client/Subscriber.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.eventbus.client; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public final class Subscriber 6 | { 7 | private final EventBusClient client; 8 | final int channelId; 9 | 10 | final String queueName; 11 | 12 | private final SubscriberListener listener; 13 | 14 | Subscriber(@NotNull EventBusClient client, int channelId, @NotNull String queueName, @NotNull SubscriberListener listener) 15 | { 16 | this.client = client; 17 | this.channelId = channelId; 18 | this.queueName = queueName; 19 | this.listener = listener; 20 | } 21 | 22 | public void close() 23 | { 24 | this.client.removeSubscriber(this.channelId); 25 | this.client.sendMessage(this.channelId, "CLOSE"); 26 | } 27 | 28 | boolean handleMessage(@NotNull String message) 29 | { 30 | if (message.equals("ERR")) 31 | { 32 | this.close(); 33 | this.listener.error(); 34 | return true; 35 | } 36 | else 37 | { 38 | String[] fields = message.split(":", 3); 39 | if (fields.length != 3) 40 | { 41 | return false; 42 | } 43 | 44 | String timestampString = fields[0]; 45 | long timestamp; 46 | try 47 | { 48 | timestamp = Long.parseLong(timestampString); 49 | } 50 | catch (NumberFormatException exception) 51 | { 52 | return false; 53 | } 54 | if (timestamp < 0) 55 | { 56 | return false; 57 | } 58 | String type = fields[1]; 59 | String data = fields[2]; 60 | 61 | this.listener.event(new Event(timestamp, type, data)); 62 | 63 | return true; 64 | } 65 | } 66 | 67 | void error() 68 | { 69 | this.listener.error(); 70 | } 71 | 72 | public interface SubscriberListener 73 | { 74 | void event(@NotNull Event event); 75 | 76 | void error(); 77 | } 78 | 79 | public static final class Event 80 | { 81 | public final long timestamp; 82 | @NotNull 83 | public final String type; 84 | @NotNull 85 | public final String data; 86 | 87 | private Event(long timestamp, @NotNull String type, @NotNull String data) 88 | { 89 | this.timestamp = timestamp; 90 | this.type = type; 91 | this.data = data; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routes/PlayerRouter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routes; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.apiserver.routes.player.BoostsRouter; 6 | import micheal65536.vienna.apiserver.routes.player.BuildplatesRouter; 7 | import micheal65536.vienna.apiserver.routes.player.ChallengesRouter; 8 | import micheal65536.vienna.apiserver.routes.player.InventoryRouter; 9 | import micheal65536.vienna.apiserver.routes.player.JournalRouter; 10 | import micheal65536.vienna.apiserver.routes.player.ProfileRouter; 11 | import micheal65536.vienna.apiserver.routes.player.TappablesRouter; 12 | import micheal65536.vienna.apiserver.routes.player.TokensRouter; 13 | import micheal65536.vienna.apiserver.routes.player.WorkshopRouter; 14 | import micheal65536.vienna.apiserver.routing.Router; 15 | import micheal65536.vienna.apiserver.utils.BuildplateInstancesManager; 16 | import micheal65536.vienna.apiserver.utils.TappablesManager; 17 | import micheal65536.vienna.db.EarthDB; 18 | import micheal65536.vienna.eventbus.client.EventBusClient; 19 | import micheal65536.vienna.objectstore.client.ObjectStoreClient; 20 | import micheal65536.vienna.staticdata.StaticData; 21 | 22 | public class PlayerRouter extends Router 23 | { 24 | public PlayerRouter(@NotNull EarthDB earthDB, @NotNull EventBusClient eventBusClient, @NotNull ObjectStoreClient objectStoreClient, @NotNull BuildplateInstancesManager buildplateInstancesManager, @NotNull StaticData staticData) 25 | { 26 | TappablesManager tappablesManager = new TappablesManager(eventBusClient); 27 | 28 | this.addSubRouter("/*", 0, new ProfileRouter(earthDB, staticData)); 29 | this.addSubRouter("/*", 0, new TokensRouter(earthDB, staticData)); 30 | this.addSubRouter("/*", 0, new InventoryRouter(earthDB, staticData.catalog)); 31 | this.addSubRouter("/*", 0, new WorkshopRouter(earthDB, staticData)); 32 | this.addSubRouter("/*", 0, new BoostsRouter(earthDB, staticData.catalog)); 33 | this.addSubRouter("/*", 0, new JournalRouter(earthDB)); 34 | this.addSubRouter("/*", 0, new BuildplatesRouter(earthDB, objectStoreClient, buildplateInstancesManager, tappablesManager, staticData.catalog)); 35 | this.addSubRouter("/*", 0, new TappablesRouter(earthDB, eventBusClient, tappablesManager, staticData)); 36 | this.addSubRouter("/*", 0, new ChallengesRouter(earthDB)); 37 | } 38 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/tappables/ActiveLocation.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.tappables; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import micheal65536.vienna.apiserver.types.common.Coordinate; 8 | import micheal65536.vienna.apiserver.types.common.Rarity; 9 | 10 | public record ActiveLocation( 11 | @NotNull String id, 12 | @NotNull String tileId, 13 | @NotNull Coordinate coordinate, 14 | @NotNull String spawnTime, 15 | @NotNull String expirationTime, 16 | @NotNull Type type, 17 | @NotNull String icon, 18 | @NotNull Metadata metadata, 19 | @Nullable TappableMetadata tappableMetadata, 20 | @Nullable EncounterMetadata encounterMetadata 21 | ) 22 | { 23 | public enum Type 24 | { 25 | @SerializedName("Tappable") TAPPABLE, 26 | @SerializedName("Encounter") ENCOUNTER, 27 | @SerializedName("PlayerAdventure") PLAYER_ADVENTURE 28 | } 29 | 30 | public record Metadata( 31 | @NotNull String rewardId, 32 | @NotNull Rarity rarity 33 | ) 34 | { 35 | } 36 | 37 | public record TappableMetadata( 38 | @NotNull Rarity rarity 39 | ) 40 | { 41 | } 42 | 43 | public record EncounterMetadata( 44 | @NotNull EncounterType encounterType, 45 | @NotNull String locationId, 46 | @NotNull String worldId, 47 | @NotNull AnchorState anchorState, 48 | @NotNull String anchorId, 49 | @NotNull String augmentedImageSetId 50 | ) 51 | { 52 | // TODO: what do these actually do? 53 | public enum EncounterType 54 | { 55 | @SerializedName("None") NONE, 56 | @SerializedName("Short4X4Peaceful") SHORT_4X4_PEACEFUL, 57 | @SerializedName("Short4X4Hostile") SHORT_4X4_HOSTILE, 58 | @SerializedName("Short8X8Peaceful") SHORT_8X8_PEACEFUL, 59 | @SerializedName("Short8X8Hostile") SHORT_8X8_HOSTILE, 60 | @SerializedName("Short16X16Peaceful") SHORT_16X16_PEACEFUL, 61 | @SerializedName("Short16X16Hostile") SHORT_16X16_HOSTILE, 62 | @SerializedName("Tall4X4Peaceful") TALL_4X4_PEACEFUL, 63 | @SerializedName("Tall4X4Hostile") TALL_4X4_HOSTILE, 64 | @SerializedName("Tall8X8Peaceful") TALL_8X8_PEACEFUL, 65 | @SerializedName("Tall8X8Hostile") TALL_8X8_HOSTILE, 66 | @SerializedName("Tall16X16Peaceful") TALL_16X16_PEACEFUL, 67 | @SerializedName("Tall16X16Hostile") TALL_16X16_HOSTILE 68 | } 69 | 70 | public enum AnchorState 71 | { 72 | @SerializedName("Off") OFF 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /objectstore/server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | objectstore-server 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | 20 | maven-assembly-plugin 21 | 3.6.0 22 | 23 | 24 | jar-with-dependencies 25 | 26 | 27 | 28 | micheal65536.vienna.objectstore.server.Main 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | package 38 | 39 | single 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | commons-cli 50 | commons-cli 51 | 1.6.0 52 | 53 | 54 | org.apache.logging.log4j 55 | log4j-api 56 | 2.22.1 57 | 58 | 59 | org.apache.logging.log4j 60 | log4j-core 61 | 2.22.1 62 | 63 | 64 | 65 | org.jetbrains 66 | annotations 67 | 24.1.0 68 | provided 69 | 70 | 71 | -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/utils/TokenUtils.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.utils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.db.EarthDB; 6 | import micheal65536.vienna.db.model.player.ActivityLog; 7 | import micheal65536.vienna.db.model.player.Tokens; 8 | import micheal65536.vienna.staticdata.StaticData; 9 | 10 | import java.util.UUID; 11 | 12 | public final class TokenUtils 13 | { 14 | @NotNull 15 | public static EarthDB.Query addToken(@NotNull String playerId, @NotNull Tokens.Token token) 16 | { 17 | EarthDB.Query getQuery = new EarthDB.Query(true); 18 | getQuery.get("tokens", playerId, Tokens.class); 19 | getQuery.then(results -> 20 | { 21 | Tokens tokens = (Tokens) results.get("tokens").value(); 22 | String id = UUID.randomUUID().toString(); 23 | tokens.addToken(id, token); 24 | EarthDB.Query updateQuery = new EarthDB.Query(true); 25 | updateQuery.update("tokens", playerId, tokens); 26 | updateQuery.extra("tokenId", id); 27 | return updateQuery; 28 | }); 29 | return getQuery; 30 | } 31 | 32 | // does not handle redeeming the token itself (removing it from the list of tokens belonging to the player) 33 | @NotNull 34 | public static EarthDB.Query doActionsOnRedeemedToken(@NotNull Tokens.Token token, @NotNull String playerId, long currentTime, @NotNull StaticData staticData) 35 | { 36 | EarthDB.Query getQuery = new EarthDB.Query(true); 37 | 38 | switch (token.type) 39 | { 40 | case LEVEL_UP -> 41 | { 42 | Tokens.LevelUpToken levelUpToken = (Tokens.LevelUpToken) token; 43 | getQuery.then(results -> 44 | { 45 | EarthDB.Query updateQuery = new EarthDB.Query(true); 46 | 47 | updateQuery.then(ActivityLogUtils.addEntry(playerId, new ActivityLog.LevelUpEntry(currentTime, levelUpToken.level))); 48 | 49 | updateQuery.then(Rewards.fromDBRewardsModel(levelUpToken.rewards).toRedeemQuery(playerId, currentTime, staticData)); 50 | 51 | return updateQuery; 52 | }, false); 53 | } 54 | case JOURNAL_ITEM_UNLOCKED -> 55 | { 56 | Tokens.JournalItemUnlockedToken journalItemUnlockedToken = (Tokens.JournalItemUnlockedToken) token; 57 | getQuery.then(results -> 58 | { 59 | EarthDB.Query updateQuery = new EarthDB.Query(true); 60 | 61 | updateQuery.then(ActivityLogUtils.addEntry(playerId, new ActivityLog.JournalItemUnlockedEntry(currentTime, journalItemUnlockedToken.itemId))); 62 | 63 | /*int experiencePoints = staticData.catalog.itemsCatalog.getItem(journalItemUnlockedToken.itemId).experience().journal(); 64 | if (experiencePoints > 0) 65 | { 66 | updateQuery.then(new Rewards().addExperiencePoints(experiencePoints).toRedeemQuery(playerId, currentTime, staticData)); 67 | }*/ 68 | 69 | return updateQuery; 70 | }, false); 71 | } 72 | } 73 | 74 | getQuery.extra("token", token); 75 | 76 | return getQuery; 77 | } 78 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routes/AuthenticatedRouter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routes; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.apiserver.routing.Filter; 6 | import micheal65536.vienna.apiserver.routing.Request; 7 | import micheal65536.vienna.apiserver.routing.Response; 8 | import micheal65536.vienna.apiserver.routing.Router; 9 | import micheal65536.vienna.apiserver.utils.BuildplateInstancesManager; 10 | import micheal65536.vienna.db.EarthDB; 11 | import micheal65536.vienna.eventbus.client.EventBusClient; 12 | import micheal65536.vienna.objectstore.client.ObjectStoreClient; 13 | import micheal65536.vienna.staticdata.StaticData; 14 | 15 | public class AuthenticatedRouter extends Router 16 | { 17 | public AuthenticatedRouter(@NotNull EarthDB earthDB, @NotNull StaticData staticData, @NotNull EventBusClient eventBusClient, @NotNull ObjectStoreClient objectStoreClient, @NotNull BuildplateInstancesManager buildplateInstancesManager) 18 | { 19 | Filter authFilter = request -> 20 | { 21 | String sessionId = request.getParameter("sessionId"); 22 | String authorization = request.getParameter("authorization"); 23 | String[] parts = authorization.split(" "); 24 | if (parts.length != 2 || !parts[0].equals("Genoa")) 25 | { 26 | return Response.badRequest(); 27 | } 28 | String sessionToken = parts[1]; 29 | 30 | // TODO: check session token and properly get player ID 31 | String playerId = sessionToken; 32 | 33 | request.addContextData("playerId", playerId); 34 | return null; 35 | }; 36 | this.addFilter( 37 | new Route.Builder(Request.Method.HEAD, "/*").addHeaderParameter("sessionId", "Session-Id").addHeaderParameter("authorization", "Authorization").build(), 38 | authFilter 39 | ); 40 | this.addFilter( 41 | new Route.Builder(Request.Method.GET, "/*").addHeaderParameter("sessionId", "Session-Id").addHeaderParameter("authorization", "Authorization").build(), 42 | authFilter 43 | ); 44 | this.addFilter( 45 | new Route.Builder(Request.Method.POST, "/*").addHeaderParameter("sessionId", "Session-Id").addHeaderParameter("authorization", "Authorization").build(), 46 | authFilter 47 | ); 48 | this.addFilter( 49 | new Route.Builder(Request.Method.PUT, "/*").addHeaderParameter("sessionId", "Session-Id").addHeaderParameter("authorization", "Authorization").build(), 50 | authFilter 51 | ); 52 | this.addFilter( 53 | new Route.Builder(Request.Method.DELETE, "/*").addHeaderParameter("sessionId", "Session-Id").addHeaderParameter("authorization", "Authorization").build(), 54 | authFilter 55 | ); 56 | 57 | this.addSubRouter("/*", 0, new PlayerRouter(earthDB, eventBusClient, objectStoreClient, buildplateInstancesManager, staticData)); 58 | this.addSubRouter("/*", 0, new CatalogRouter(staticData.catalog)); 59 | this.addSubRouter("/*", 0, new EnvironmentSettingsRouter()); 60 | } 61 | } -------------------------------------------------------------------------------- /objectstore/server/src/main/java/micheal65536/vienna/objectstore/server/DataStore.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.objectstore.server; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.File; 8 | import java.io.FileInputStream; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.util.UUID; 13 | 14 | public class DataStore 15 | { 16 | private final File rootDirectory; 17 | 18 | public DataStore(@NotNull File rootDirectory) throws DataStoreException 19 | { 20 | this.rootDirectory = rootDirectory; 21 | if (!this.rootDirectory.isDirectory() || !this.rootDirectory.canRead()) 22 | { 23 | throw new DataStoreException("Data root directory %s is not a directory or cannot be read".formatted(this.rootDirectory.getPath())); 24 | } 25 | LogManager.getLogger().info("Opened data store from {}", this.rootDirectory.getPath()); 26 | } 27 | 28 | @NotNull 29 | public String store(byte[] data) throws DataStoreException 30 | { 31 | String id = UUID.randomUUID().toString(); 32 | 33 | File file = new File(this.rootDirectory, id.substring(0, 2)); 34 | if (!file.isDirectory()) 35 | { 36 | file.mkdir(); 37 | } 38 | file = new File(file, id); 39 | 40 | try (FileOutputStream fileOutputStream = new FileOutputStream(file)) 41 | { 42 | fileOutputStream.write(data); 43 | } 44 | catch (IOException exception) 45 | { 46 | file.delete(); 47 | throw new DataStoreException(exception); 48 | } 49 | 50 | return id; 51 | } 52 | 53 | public byte[] load(@NotNull String id) throws DataStoreException 54 | { 55 | File file = new File(new File(this.rootDirectory, id.substring(0, 2)), id); 56 | if (!file.exists()) 57 | { 58 | return null; 59 | } 60 | 61 | ByteArrayOutputStream byteArrayOutputStream; 62 | try 63 | { 64 | byteArrayOutputStream = new ByteArrayOutputStream((int) Files.size(file.toPath())); 65 | } 66 | catch (IOException exception) 67 | { 68 | throw new DataStoreException(exception); 69 | } 70 | 71 | try (FileInputStream fileInputStream = new FileInputStream(file)) 72 | { 73 | fileInputStream.transferTo(byteArrayOutputStream); 74 | } 75 | catch (IOException exception) 76 | { 77 | throw new DataStoreException(exception); 78 | } 79 | 80 | byte[] data = byteArrayOutputStream.toByteArray(); 81 | 82 | return data; 83 | } 84 | 85 | public void delete(@NotNull String id) 86 | { 87 | File file = new File(new File(this.rootDirectory, id.substring(0, 2)), id); 88 | file.delete(); 89 | } 90 | 91 | public static final class DataStoreException extends Exception 92 | { 93 | private DataStoreException(String message) 94 | { 95 | super(message); 96 | } 97 | 98 | private DataStoreException(Throwable cause) 99 | { 100 | super(cause); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /eventbus/server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | eventbus-server 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | 20 | maven-assembly-plugin 21 | 3.6.0 22 | 23 | 24 | jar-with-dependencies 25 | 26 | 27 | 28 | micheal65536.vienna.eventbus.server.Main 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | package 38 | 39 | single 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | commons-cli 50 | commons-cli 51 | 1.6.0 52 | 53 | 54 | org.apache.logging.log4j 55 | log4j-api 56 | 2.22.1 57 | 58 | 59 | org.apache.logging.log4j 60 | log4j-core 61 | 2.22.1 62 | 63 | 64 | 65 | com.google.code.gson 66 | gson 67 | 2.10.1 68 | 69 | 70 | 71 | org.jetbrains 72 | annotations 73 | 24.1.0 74 | provided 75 | 76 | 77 | -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/Tokens.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | import com.google.gson.JsonDeserializationContext; 4 | import com.google.gson.JsonDeserializer; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonParseException; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import micheal65536.vienna.db.model.common.Rewards; 11 | 12 | import java.util.HashMap; 13 | 14 | public final class Tokens 15 | { 16 | @NotNull 17 | private final HashMap tokens; 18 | 19 | public Tokens() 20 | { 21 | this.tokens = new HashMap<>(); 22 | } 23 | 24 | @NotNull 25 | public Tokens copy() 26 | { 27 | Tokens tokens = new Tokens(); 28 | tokens.tokens.putAll(this.tokens); 29 | return tokens; 30 | } 31 | 32 | public record TokenWithId( 33 | @NotNull String id, 34 | @NotNull Token token 35 | ) 36 | { 37 | } 38 | 39 | @NotNull 40 | public TokenWithId[] getTokens() 41 | { 42 | return this.tokens.entrySet().stream().map(entry -> new TokenWithId(entry.getKey(), entry.getValue())).toArray(TokenWithId[]::new); 43 | } 44 | 45 | public void addToken(@NotNull String id, @NotNull Token token) 46 | { 47 | this.tokens.put(id, token); 48 | } 49 | 50 | @Nullable 51 | public Token removeToken(@NotNull String id) 52 | { 53 | return this.tokens.remove(id); 54 | } 55 | 56 | public static abstract class Token 57 | { 58 | @NotNull 59 | public final Type type; 60 | 61 | private Token(@NotNull Type type) 62 | { 63 | this.type = type; 64 | } 65 | 66 | public enum Type 67 | { 68 | LEVEL_UP, 69 | JOURNAL_ITEM_UNLOCKED 70 | } 71 | 72 | public static class Deserializer implements JsonDeserializer 73 | { 74 | private static class BaseToken extends Token 75 | { 76 | private BaseToken(@NotNull Type type) 77 | { 78 | super(type); 79 | } 80 | } 81 | 82 | @Override 83 | public Token deserialize(JsonElement jsonElement, java.lang.reflect.Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException 84 | { 85 | BaseToken baseToken = jsonDeserializationContext.deserialize(jsonElement, BaseToken.class); 86 | return jsonDeserializationContext.deserialize(jsonElement, switch (baseToken.type) 87 | { 88 | case LEVEL_UP -> LevelUpToken.class; 89 | case JOURNAL_ITEM_UNLOCKED -> JournalItemUnlockedToken.class; 90 | }); 91 | } 92 | } 93 | } 94 | 95 | public static final class LevelUpToken extends Token 96 | { 97 | public final int level; 98 | public final Rewards rewards; 99 | 100 | public LevelUpToken(int level, @NotNull Rewards rewards) 101 | { 102 | super(Type.LEVEL_UP); 103 | this.level = level; 104 | this.rewards = rewards; 105 | } 106 | } 107 | 108 | public static final class JournalItemUnlockedToken extends Token 109 | { 110 | @NotNull 111 | public final String itemId; 112 | 113 | public JournalItemUnlockedToken(@NotNull String itemId) 114 | { 115 | super(Type.JOURNAL_ITEM_UNLOCKED); 116 | this.itemId = itemId; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /tappablesgenerator/src/main/java/micheal65536/vienna/tappablesgenerator/EncounterGenerator.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.tappablesgenerator; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import micheal65536.vienna.staticdata.EncountersConfig; 7 | import micheal65536.vienna.staticdata.StaticData; 8 | 9 | import java.util.Arrays; 10 | import java.util.LinkedList; 11 | import java.util.Random; 12 | import java.util.UUID; 13 | 14 | public class EncounterGenerator 15 | { 16 | // TODO: make these configurable 17 | private static final int CHANCE_PER_TILE = 4; 18 | private static final long MIN_DELAY = 1 * 60 * 1000; 19 | private static final long MAX_DELAY = 2 * 60 * 1000; 20 | 21 | private final StaticData staticData; 22 | private final int maxDuration; 23 | 24 | private final Random random; 25 | 26 | public EncounterGenerator(@NotNull StaticData staticData) 27 | { 28 | this.staticData = staticData; 29 | if (this.staticData.encountersConfig.encounters.length == 0) 30 | { 31 | LogManager.getLogger().warn("No encounter configs provided"); 32 | } 33 | this.maxDuration = Arrays.stream(this.staticData.encountersConfig.encounters).mapToInt(encounterConfig -> encounterConfig.duration()).max().orElse(0) * 1000; 34 | 35 | this.random = new Random(); 36 | } 37 | 38 | public long getMaxEncounterLifetime() 39 | { 40 | return MAX_DELAY + this.maxDuration + 30 * 1000; 41 | } 42 | 43 | @NotNull 44 | public Encounter[] generateEncounters(int tileX, int tileY, long currentTime) 45 | { 46 | if (this.staticData.encountersConfig.encounters.length == 0) 47 | { 48 | return new Encounter[0]; 49 | } 50 | 51 | LinkedList encounters = new LinkedList<>(); 52 | if (this.random.nextInt(0, CHANCE_PER_TILE) == 0) 53 | { 54 | long spawnDelay = this.random.nextLong(MIN_DELAY, MAX_DELAY + 1); 55 | 56 | EncountersConfig.EncounterConfig encounterConfig = this.staticData.encountersConfig.encounters[this.random.nextInt(0, this.staticData.encountersConfig.encounters.length)]; 57 | 58 | float[] tileBounds = getTileBounds(tileX, tileY); 59 | float lat = this.random.nextFloat(tileBounds[1], tileBounds[0]); 60 | float lon = this.random.nextFloat(tileBounds[2], tileBounds[3]); 61 | 62 | Encounter encounter = new Encounter( 63 | UUID.randomUUID().toString(), 64 | lat, 65 | lon, 66 | currentTime + spawnDelay, 67 | encounterConfig.duration() * 1000, 68 | encounterConfig.icon(), 69 | Encounter.Rarity.valueOf(encounterConfig.rarity().name()), 70 | encounterConfig.encounterBuildplateId() 71 | ); 72 | encounters.add(encounter); 73 | } 74 | return encounters.toArray(Encounter[]::new); 75 | } 76 | 77 | private static float[] getTileBounds(int tileX, int tileY) 78 | { 79 | return new float[]{ 80 | yToLat((float) tileY / (1 << 16)), 81 | yToLat((float) (tileY + 1) / (1 << 16)), 82 | xToLon((float) tileX / (1 << 16)), 83 | xToLon((float) (tileX + 1) / (1 << 16)) 84 | }; 85 | } 86 | 87 | private static float xToLon(float x) 88 | { 89 | return (float) Math.toDegrees((x * 2.0 - 1.0) * Math.PI); 90 | } 91 | 92 | private static float yToLat(float y) 93 | { 94 | return (float) Math.toDegrees(Math.atan(Math.sinh((1.0 - y * 2.0) * Math.PI))); 95 | } 96 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/types/catalog/ItemsCatalog.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.types.catalog; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import micheal65536.vienna.apiserver.types.common.BurnRate; 7 | import micheal65536.vienna.apiserver.types.common.Rarity; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public record ItemsCatalog( 13 | @NotNull Item[] items, 14 | @NotNull HashMap efficiencyCategories 15 | ) 16 | { 17 | public record Item( 18 | @NotNull String id, 19 | @NotNull ItemData item, 20 | @NotNull String category, 21 | @NotNull Rarity rarity, 22 | int fragmentsRequired, 23 | boolean stacks, 24 | @Nullable BurnRate burnRate, 25 | @NotNull ReturnItem[] fuelReturnItems, 26 | @NotNull ReturnItem[] consumeReturnItems, 27 | @Nullable Integer experience, 28 | @NotNull HashMap experiencePoints, 29 | boolean deprecated 30 | ) 31 | { 32 | public record ItemData( 33 | @NotNull String name, 34 | @Nullable Integer aux, 35 | @NotNull String type, 36 | @NotNull String useType, 37 | @Nullable Integer tapSpeed, 38 | @Nullable Integer heal, 39 | @Nullable Integer nutrition, 40 | @Nullable Integer mobDamage, 41 | @Nullable Integer blockDamage, 42 | @Nullable Integer health, 43 | @Nullable BlockMetadata blockMetadata, 44 | @Nullable ItemMetadata itemMetadata, 45 | @Nullable BoostMetadata boostMetadata, 46 | @Nullable JournalMetadata journalMetadata, 47 | @Nullable AudioMetadata audioMetadata, 48 | @NotNull Map clientProperties 49 | ) 50 | { 51 | public record BlockMetadata( 52 | @Nullable Integer health, 53 | @Nullable String efficiencyCategory 54 | ) 55 | { 56 | } 57 | 58 | public record ItemMetadata( 59 | @NotNull String useType, 60 | @NotNull String alternativeUseType, 61 | @Nullable Integer mobDamage, 62 | @Nullable Integer blockDamage, 63 | @Nullable Integer weakDamage, 64 | @Nullable Integer nutrition, 65 | @Nullable Integer heal, 66 | @Nullable String efficiencyType, 67 | @Nullable Integer maxHealth 68 | ) 69 | { 70 | } 71 | 72 | public record JournalMetadata( 73 | @NotNull String groupKey, 74 | int experience, 75 | int order, 76 | @NotNull String behavior, 77 | @NotNull String biome 78 | ) 79 | { 80 | } 81 | 82 | public record AudioMetadata( 83 | @NotNull HashMap sounds, 84 | @NotNull String defaultSound 85 | ) 86 | { 87 | } 88 | } 89 | 90 | public record ReturnItem( 91 | @NotNull String id, 92 | int amount 93 | ) 94 | { 95 | } 96 | } 97 | 98 | public record EfficiencyCategory( 99 | @NotNull EfficiencyMap efficiencyMap 100 | ) 101 | { 102 | public record EfficiencyMap( 103 | float hand, 104 | float hoe, 105 | float axe, 106 | float shovel, 107 | float pickaxe_1, 108 | float pickaxe_2, 109 | float pickaxe_3, 110 | float pickaxe_4, 111 | float pickaxe_5, 112 | float sword, 113 | float sheers 114 | ) 115 | { 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routing/Response.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routing; 2 | 3 | import com.google.gson.Gson; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Arrays; 8 | import java.util.HashMap; 9 | 10 | public final class Response 11 | { 12 | final int statusCode; 13 | byte[] body; 14 | String contentType; 15 | boolean isText; 16 | final HashMap extraHeaders = new HashMap<>(); 17 | 18 | private Response(int statusCode) 19 | { 20 | this.statusCode = statusCode; 21 | this.body = new byte[0]; 22 | this.isText = true; 23 | this.contentType = "text/plain"; 24 | } 25 | 26 | @NotNull 27 | public static Response create(int statusCode) 28 | { 29 | return new Response(statusCode); 30 | } 31 | 32 | @NotNull 33 | public static Response serverError() 34 | { 35 | return Response.create(500); 36 | } 37 | 38 | @NotNull 39 | public static Response badRequest() 40 | { 41 | return Response.create(400); 42 | } 43 | 44 | @NotNull 45 | public static Response notFound() 46 | { 47 | return Response.create(404); 48 | } 49 | 50 | @NotNull 51 | public static Response redirect(@NotNull String location) 52 | { 53 | return Response.create(302).header("Location", location); 54 | } 55 | 56 | @NotNull 57 | public static Response ok(@NotNull String body) 58 | { 59 | return Response.create(200).body(body); 60 | } 61 | 62 | @NotNull 63 | public static Response ok(byte[] body, @NotNull String contentType) 64 | { 65 | return Response.create(200).body(body, contentType); 66 | } 67 | 68 | @NotNull 69 | public static Response okFromJson(@NotNull T body, @NotNull Class tClass) 70 | { 71 | return Response.create(200).bodyFromJson(body, tClass); 72 | } 73 | 74 | @NotNull 75 | public Response body(@NotNull String body) 76 | { 77 | this.body = body.getBytes(StandardCharsets.UTF_8); 78 | this.contentType = "text/plain"; 79 | this.isText = true; 80 | return this; 81 | } 82 | 83 | @NotNull 84 | public Response body(@NotNull String body, @NotNull String contentType) 85 | { 86 | this.body = body.getBytes(StandardCharsets.UTF_8); 87 | this.contentType = contentType; 88 | this.isText = true; 89 | return this; 90 | } 91 | 92 | @NotNull 93 | public Response body(byte[] body, @NotNull String contentType) 94 | { 95 | this.body = Arrays.copyOf(body, body.length); 96 | this.contentType = contentType; 97 | this.isText = false; 98 | return this; 99 | } 100 | 101 | @NotNull 102 | public Response bodyFromJson(@NotNull T body, @NotNull Class tClass) 103 | { 104 | String bodyString = new Gson().newBuilder().serializeNulls().create().toJson(body, tClass); 105 | this.body = bodyString.getBytes(StandardCharsets.UTF_8); 106 | this.contentType = "application/json"; 107 | this.isText = true; 108 | return this; 109 | } 110 | 111 | @NotNull 112 | public Response contentType(@NotNull String contentType) 113 | { 114 | this.contentType = contentType; 115 | return this; 116 | } 117 | 118 | @NotNull 119 | public Response header(@NotNull String name, @NotNull String value) 120 | { 121 | this.extraHeaders.put(name, value); 122 | return this; 123 | } 124 | } -------------------------------------------------------------------------------- /utils/http-routing/src/main/java/micheal65536/vienna/utils/http/routing/Response.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.utils.http.routing; 2 | 3 | import com.google.gson.Gson; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Arrays; 8 | import java.util.HashMap; 9 | 10 | public final class Response 11 | { 12 | final int statusCode; 13 | byte[] body; 14 | String contentType; 15 | boolean isText; 16 | final HashMap extraHeaders = new HashMap<>(); 17 | 18 | private Response(int statusCode) 19 | { 20 | this.statusCode = statusCode; 21 | this.body = new byte[0]; 22 | this.isText = true; 23 | this.contentType = "text/plain"; 24 | } 25 | 26 | @NotNull 27 | public static Response create(int statusCode) 28 | { 29 | return new Response(statusCode); 30 | } 31 | 32 | @NotNull 33 | public static Response serverError() 34 | { 35 | return Response.create(500); 36 | } 37 | 38 | @NotNull 39 | public static Response badRequest() 40 | { 41 | return Response.create(400); 42 | } 43 | 44 | @NotNull 45 | public static Response notFound() 46 | { 47 | return Response.create(404); 48 | } 49 | 50 | @NotNull 51 | public static Response redirect(@NotNull String location) 52 | { 53 | return Response.create(302).header("Location", location); 54 | } 55 | 56 | @NotNull 57 | public static Response ok(@NotNull String body) 58 | { 59 | return Response.create(200).body(body); 60 | } 61 | 62 | @NotNull 63 | public static Response ok(byte[] body, @NotNull String contentType) 64 | { 65 | return Response.create(200).body(body, contentType); 66 | } 67 | 68 | @NotNull 69 | public static Response okFromJson(@NotNull T body, @NotNull Class tClass) 70 | { 71 | return Response.create(200).bodyFromJson(body, tClass); 72 | } 73 | 74 | @NotNull 75 | public Response body(@NotNull String body) 76 | { 77 | this.body = body.getBytes(StandardCharsets.UTF_8); 78 | this.contentType = "text/plain"; 79 | this.isText = true; 80 | return this; 81 | } 82 | 83 | @NotNull 84 | public Response body(@NotNull String body, @NotNull String contentType) 85 | { 86 | this.body = body.getBytes(StandardCharsets.UTF_8); 87 | this.contentType = contentType; 88 | this.isText = true; 89 | return this; 90 | } 91 | 92 | @NotNull 93 | public Response body(byte[] body, @NotNull String contentType) 94 | { 95 | this.body = Arrays.copyOf(body, body.length); 96 | this.contentType = contentType; 97 | this.isText = false; 98 | return this; 99 | } 100 | 101 | @NotNull 102 | public Response bodyFromJson(@NotNull T body, @NotNull Class tClass) 103 | { 104 | String bodyString = new Gson().newBuilder().serializeNulls().create().toJson(body, tClass); 105 | this.body = bodyString.getBytes(StandardCharsets.UTF_8); 106 | this.contentType = "application/json"; 107 | this.isText = true; 108 | return this; 109 | } 110 | 111 | @NotNull 112 | public Response contentType(@NotNull String contentType) 113 | { 114 | this.contentType = contentType; 115 | return this; 116 | } 117 | 118 | @NotNull 119 | public Response header(@NotNull String name, @NotNull String value) 120 | { 121 | this.extraHeaders.put(name, value); 122 | return this; 123 | } 124 | } -------------------------------------------------------------------------------- /utils/cdn/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | utils-cdn 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | 20 | maven-assembly-plugin 21 | 3.6.0 22 | 23 | 24 | jar-with-dependencies 25 | 26 | 27 | 28 | micheal65536.vienna.utils.cdn.Main 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | package 38 | 39 | single 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.apache.tomcat.embed 50 | tomcat-embed-core 51 | 10.1.19 52 | 53 | 54 | micheal65536.vienna 55 | utils-http-routing 56 | 0.0.1-SNAPSHOT 57 | 58 | 59 | 60 | com.google.code.gson 61 | gson 62 | 2.10.1 63 | 64 | 65 | 66 | commons-cli 67 | commons-cli 68 | 1.6.0 69 | 70 | 71 | org.apache.logging.log4j 72 | log4j-api 73 | 2.22.1 74 | 75 | 76 | org.apache.logging.log4j 77 | log4j-core 78 | 2.22.1 79 | 80 | 81 | 82 | org.jetbrains 83 | annotations 84 | 24.1.0 85 | provided 86 | 87 | 88 | -------------------------------------------------------------------------------- /utils/locator/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | utils-locator 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | 20 | maven-assembly-plugin 21 | 3.6.0 22 | 23 | 24 | jar-with-dependencies 25 | 26 | 27 | 28 | micheal65536.vienna.utils.locator.Main 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | package 38 | 39 | single 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.apache.tomcat.embed 50 | tomcat-embed-core 51 | 10.1.19 52 | 53 | 54 | micheal65536.vienna 55 | utils-http-routing 56 | 0.0.1-SNAPSHOT 57 | 58 | 59 | 60 | com.google.code.gson 61 | gson 62 | 2.10.1 63 | 64 | 65 | 66 | commons-cli 67 | commons-cli 68 | 1.6.0 69 | 70 | 71 | org.apache.logging.log4j 72 | log4j-api 73 | 2.22.1 74 | 75 | 76 | org.apache.logging.log4j 77 | log4j-core 78 | 2.22.1 79 | 80 | 81 | 82 | org.jetbrains 83 | annotations 84 | 24.1.0 85 | provided 86 | 87 | 88 | -------------------------------------------------------------------------------- /tappablesgenerator/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | tappablesgenerator 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | 20 | maven-assembly-plugin 21 | 3.6.0 22 | 23 | 24 | jar-with-dependencies 25 | 26 | 27 | 28 | micheal65536.vienna.tappablesgenerator.Main 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | package 38 | 39 | single 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | commons-cli 50 | commons-cli 51 | 1.6.0 52 | 53 | 54 | org.apache.logging.log4j 55 | log4j-api 56 | 2.22.1 57 | 58 | 59 | org.apache.logging.log4j 60 | log4j-core 61 | 2.22.1 62 | 63 | 64 | 65 | micheal65536.vienna 66 | staticdata 67 | 0.0.1-SNAPSHOT 68 | 69 | 70 | micheal65536.vienna 71 | eventbus-client 72 | 0.0.1-SNAPSHOT 73 | 74 | 75 | 76 | com.google.code.gson 77 | gson 78 | 2.10.1 79 | 80 | 81 | 82 | org.jetbrains 83 | annotations 84 | 24.1.0 85 | provided 86 | 87 | 88 | -------------------------------------------------------------------------------- /eventbus/client/src/main/java/micheal65536/vienna/eventbus/client/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.eventbus.client; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public final class RequestHandler 9 | { 10 | private final EventBusClient client; 11 | private final int channelId; 12 | 13 | private final String queueName; 14 | 15 | private final Handler handler; 16 | 17 | private volatile boolean closed = false; 18 | 19 | RequestHandler(@NotNull EventBusClient client, int channelId, @NotNull String queueName, @NotNull Handler handler) 20 | { 21 | this.client = client; 22 | this.channelId = channelId; 23 | this.queueName = queueName; 24 | this.handler = handler; 25 | } 26 | 27 | public void close() 28 | { 29 | this.closed = true; 30 | this.client.removeSubscriber(this.channelId); 31 | this.client.sendMessage(this.channelId, "CLOSE"); 32 | } 33 | 34 | boolean handleMessage(@NotNull String message) 35 | { 36 | if (message.equals("ERR")) 37 | { 38 | this.close(); 39 | this.handler.error(); 40 | return true; 41 | } 42 | else 43 | { 44 | String[] fields = message.split(":", 4); 45 | if (fields.length != 4) 46 | { 47 | return false; 48 | } 49 | 50 | String requestIdString = fields[0]; 51 | int requestId; 52 | try 53 | { 54 | requestId = Integer.parseInt(requestIdString); 55 | } 56 | catch (NumberFormatException exception) 57 | { 58 | return false; 59 | } 60 | if (requestId <= 0) 61 | { 62 | return false; 63 | } 64 | String timestampString = fields[1]; 65 | long timestamp; 66 | try 67 | { 68 | timestamp = Long.parseLong(timestampString); 69 | } 70 | catch (NumberFormatException exception) 71 | { 72 | return false; 73 | } 74 | if (timestamp < 0) 75 | { 76 | return false; 77 | } 78 | String type = fields[2]; 79 | String data = fields[3]; 80 | 81 | CompletableFuture responseCompletableFuture = this.handler.requestAsync(new Request(timestamp, type, data)); 82 | responseCompletableFuture.thenAccept(response -> 83 | { 84 | if (!this.closed) 85 | { 86 | if (response != null) 87 | { 88 | this.client.sendMessage(this.channelId, "REP " + requestId + ":" + response); 89 | } 90 | else 91 | { 92 | this.client.sendMessage(this.channelId, "NREP " + requestId); 93 | } 94 | } 95 | }); 96 | 97 | return true; 98 | } 99 | } 100 | 101 | void error() 102 | { 103 | this.closed = true; 104 | this.handler.error(); 105 | } 106 | 107 | public interface Handler 108 | { 109 | @NotNull 110 | default CompletableFuture requestAsync(@NotNull Request request) 111 | { 112 | CompletableFuture completableFuture = new CompletableFuture<>(); 113 | new Thread(() -> 114 | { 115 | completableFuture.complete(this.request(request)); 116 | }).start(); 117 | return completableFuture; 118 | } 119 | 120 | @Nullable 121 | String request(@NotNull Request request); 122 | 123 | void error(); 124 | } 125 | 126 | public static final class Request 127 | { 128 | public final long timestamp; 129 | @NotNull 130 | public final String type; 131 | @NotNull 132 | public final String data; 133 | 134 | private Request(long timestamp, @NotNull String type, @NotNull String data) 135 | { 136 | this.timestamp = timestamp; 137 | this.type = type; 138 | this.data = data; 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /tappablesgenerator/src/main/java/micheal65536/vienna/tappablesgenerator/Main.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.tappablesgenerator; 2 | 3 | import org.apache.commons.cli.CommandLine; 4 | import org.apache.commons.cli.DefaultParser; 5 | import org.apache.commons.cli.Option; 6 | import org.apache.commons.cli.Options; 7 | import org.apache.commons.cli.ParseException; 8 | import org.apache.logging.log4j.Level; 9 | import org.apache.logging.log4j.LogManager; 10 | import org.apache.logging.log4j.core.config.Configurator; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import micheal65536.vienna.eventbus.client.EventBusClient; 14 | import micheal65536.vienna.eventbus.client.EventBusClientException; 15 | import micheal65536.vienna.staticdata.StaticData; 16 | import micheal65536.vienna.staticdata.StaticDataException; 17 | 18 | import java.io.File; 19 | 20 | public class Main 21 | { 22 | public static void main(String[] args) 23 | { 24 | Configurator.setRootLevel(Level.DEBUG); 25 | 26 | Options options = new Options(); 27 | options.addOption(Option.builder() 28 | .option("staticData") 29 | .hasArg() 30 | .argName("dir") 31 | .desc("Static data path, defaults to ./data") 32 | .build()); 33 | options.addOption(Option.builder() 34 | .option("eventbus") 35 | .hasArg() 36 | .argName("eventbus") 37 | .desc("Event bus address, defaults to localhost:5532") 38 | .build()); 39 | CommandLine commandLine; 40 | String staticDataPath; 41 | String eventBusConnectionString; 42 | try 43 | { 44 | commandLine = new DefaultParser().parse(options, args); 45 | staticDataPath = commandLine.hasOption("staticData") ? commandLine.getOptionValue("staticData") : "./data"; 46 | eventBusConnectionString = commandLine.hasOption("eventbus") ? commandLine.getOptionValue("eventbus") : "localhost:5532"; 47 | } 48 | catch (ParseException exception) 49 | { 50 | LogManager.getLogger().fatal(exception); 51 | System.exit(1); 52 | return; 53 | } 54 | 55 | LogManager.getLogger().info("Loading static data"); 56 | StaticData staticData; 57 | try 58 | { 59 | staticData = new StaticData(new File(staticDataPath)); 60 | } 61 | catch (StaticDataException staticDataException) 62 | { 63 | LogManager.getLogger().fatal("Failed to load static data", staticDataException); 64 | System.exit(1); 65 | return; 66 | } 67 | LogManager.getLogger().info("Loaded static data"); 68 | 69 | LogManager.getLogger().info("Connecting to event bus"); 70 | EventBusClient eventBusClient; 71 | try 72 | { 73 | eventBusClient = EventBusClient.create(eventBusConnectionString); 74 | } 75 | catch (EventBusClientException exception) 76 | { 77 | LogManager.getLogger().fatal("Could not connect to event bus", exception); 78 | System.exit(1); 79 | return; 80 | } 81 | LogManager.getLogger().info("Connected to event bus"); 82 | 83 | TappableGenerator tappableGenerator = new TappableGenerator(staticData); 84 | EncounterGenerator encounterGenerator = new EncounterGenerator(staticData); 85 | Spawner[] spawner = new Spawner[1]; 86 | ActiveTiles activeTiles = new ActiveTiles(eventBusClient, new ActiveTiles.ActiveTileListener() 87 | { 88 | @Override 89 | public void active(@NotNull ActiveTiles.ActiveTile[] activeTiles) 90 | { 91 | spawner[0].spawnTiles(activeTiles); 92 | } 93 | 94 | @Override 95 | public void inactive(@NotNull ActiveTiles.ActiveTile[] activeTiles) 96 | { 97 | // empty 98 | } 99 | }); 100 | spawner[0] = new Spawner(eventBusClient, activeTiles, tappableGenerator, encounterGenerator); 101 | spawner[0].run(); 102 | } 103 | } -------------------------------------------------------------------------------- /utils/tools/buildplate-importer/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | utils-tools-buildplate-importer 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | 20 | maven-assembly-plugin 21 | 3.6.0 22 | 23 | 24 | jar-with-dependencies 25 | 26 | 27 | 28 | micheal65536.vienna.utils.tools.buildplate_importer.Main 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | package 38 | 39 | single 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | micheal65536.vienna 50 | db 51 | 0.0.1-SNAPSHOT 52 | 53 | 54 | micheal65536.vienna 55 | objectstore-client 56 | 0.0.1-SNAPSHOT 57 | 58 | 59 | micheal65536.vienna 60 | eventbus-client 61 | 0.0.1-SNAPSHOT 62 | 63 | 64 | 65 | com.google.code.gson 66 | gson 67 | 2.10.1 68 | 69 | 70 | 71 | commons-cli 72 | commons-cli 73 | 1.6.0 74 | 75 | 76 | org.apache.logging.log4j 77 | log4j-api 78 | 2.22.1 79 | 80 | 81 | org.apache.logging.log4j 82 | log4j-core 83 | 2.22.1 84 | 85 | 86 | 87 | org.jetbrains 88 | annotations 89 | 24.1.0 90 | provided 91 | 92 | 93 | -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routes/player/ChallengesRouter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routes.player; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import micheal65536.vienna.apiserver.routing.Request; 7 | import micheal65536.vienna.apiserver.routing.Response; 8 | import micheal65536.vienna.apiserver.routing.Router; 9 | import micheal65536.vienna.apiserver.types.common.Rarity; 10 | import micheal65536.vienna.apiserver.types.common.Rewards; 11 | import micheal65536.vienna.apiserver.utils.EarthApiResponse; 12 | import micheal65536.vienna.apiserver.utils.MapBuilder; 13 | import micheal65536.vienna.apiserver.utils.TimeFormatter; 14 | import micheal65536.vienna.db.EarthDB; 15 | 16 | public class ChallengesRouter extends Router 17 | { 18 | public ChallengesRouter(@NotNull EarthDB earthDB) 19 | { 20 | this.addHandler(new Route.Builder(Request.Method.GET, "/player/challenges").build(), request -> 21 | { 22 | // TODO: this is currently just a stub required for the journal to load properly in the client 23 | record Challenge( 24 | @NotNull String referenceId, 25 | @Nullable String parentId, 26 | @NotNull String groupId, 27 | @NotNull String duration, 28 | @NotNull String type, 29 | @NotNull String category, 30 | @Nullable Rarity rarity, 31 | int order, 32 | @NotNull String endTimeUtc, 33 | @NotNull String state, 34 | boolean isComplete, 35 | int percentComplete, 36 | int currentCount, 37 | int totalThreshold, 38 | @NotNull String[] prerequisiteIds, 39 | @NotNull String prerequisiteLogicalCondition, 40 | @NotNull Rewards rewards, 41 | @NotNull Object clientProperties 42 | ) 43 | { 44 | } 45 | return Response.okFromJson(new EarthApiResponse<>(new MapBuilder<>() 46 | .put("challenges", new MapBuilder() 47 | // client requires two season challenges with these specific persona item reward UUIDs to exist in order for the journal to load, and no one has any idea why 48 | .put("00000000-0000-0000-0000-000000000001", new Challenge( 49 | "00000000-0000-0000-0000-000000000001", 50 | null, 51 | "00000000-0000-0000-0000-000000000001", 52 | "Season", 53 | "Regular", 54 | "season_1", 55 | null, 56 | 0, 57 | TimeFormatter.formatTime(System.currentTimeMillis() + 24 * 60 * 60 * 1000), 58 | "Locked", 59 | false, 60 | 0, 61 | 0, 62 | 1, 63 | new String[0], 64 | "And", 65 | new Rewards(null, null, null, new Rewards.Item[0], new String[0], new Rewards.Challenge[0], new String[]{"230f5996-04b2-4f0e-83e5-4056c7f1d946"}, new Rewards.UtilityBlock[0]), 66 | new Object() 67 | )) 68 | .put("00000000-0000-0000-0000-000000000002", new Challenge( 69 | "00000000-0000-0000-0000-000000000002", 70 | null, 71 | "00000000-0000-0000-0000-000000000001", 72 | "Season", 73 | "Regular", 74 | "season_1", 75 | null, 76 | 0, 77 | TimeFormatter.formatTime(System.currentTimeMillis() + 24 * 60 * 60 * 1000), 78 | "Locked", 79 | false, 80 | 0, 81 | 0, 82 | 1, 83 | new String[0], 84 | "And", 85 | new Rewards(null, null, null, new Rewards.Item[0], new String[0], new Rewards.Challenge[0], new String[]{"d7725840-4376-44fc-9220-585f45775371"}, new Rewards.UtilityBlock[0]), 86 | new Object() 87 | )) 88 | .getMap() 89 | ) 90 | .put("activeSeasonChallenge", "00000000-0000-0000-0000-000000000000") 91 | .getMap() 92 | ), EarthApiResponse.class); 93 | }); 94 | } 95 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routes/EnvironmentSettingsRouter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routes; 2 | 3 | import micheal65536.vienna.apiserver.routing.Request; 4 | import micheal65536.vienna.apiserver.routing.Response; 5 | import micheal65536.vienna.apiserver.routing.Router; 6 | import micheal65536.vienna.apiserver.utils.EarthApiResponse; 7 | import micheal65536.vienna.apiserver.utils.MapBuilder; 8 | 9 | public class EnvironmentSettingsRouter extends Router 10 | { 11 | public EnvironmentSettingsRouter() 12 | { 13 | this.addHandler(new Route.Builder(Request.Method.GET, "/features").build(), request -> Response.okFromJson(new EarthApiResponse<>(new MapBuilder<>() 14 | .put("workshop_enabled", true) 15 | .put("buildplates_enabled", true) 16 | .put("enable_ruby_purchasing", true) 17 | .put("commerce_enabled", true) 18 | .put("full_logging_enabled", true) 19 | .put("challenges_enabled", true) 20 | .put("craftingv2_enabled", true) 21 | .put("smeltingv2_enabled", true) 22 | .put("inventory_item_boosts_enabled", true) 23 | .put("player_health_enabled", true) 24 | .put("minifigs_enabled", true) 25 | .put("potions_enabled", true) 26 | .put("social_link_launch_enabled", true) 27 | .put("social_link_share_enabled", true) 28 | .put("encoded_join_enabled", true) 29 | .put("adventure_crystals_enabled", true) 30 | .put("item_limits_enabled", true) 31 | .put("adventure_crystals_ftue_enabled", true) 32 | .put("expire_crystals_on_cleanup_enabled", true) 33 | .put("challenges_v2_enabled", true) 34 | .put("player_journal_enabled", true) 35 | .put("player_stats_enabled", true) 36 | .put("activity_log_enabled", true) 37 | .put("seasons_enabled", false) 38 | .put("daily_login_enabled", true) 39 | .put("store_pdp_enabled", true) 40 | .put("hotbar_stacksplitting_enabled", true) 41 | .put("fancy_rewards_screen_enabled", true) 42 | .put("async_ecs_dispatcher", true) 43 | .put("adventure_oobe_enabled", true) 44 | .put("tappable_oobe_enabled", true) 45 | .put("map_permission_oobe_enabled", true) 46 | .put("journal_oobe_enabled", true) 47 | .put("freedom_oobe_enabled", true) 48 | .put("challenge_oobe_enabled", true) 49 | .put("level_rewards_v2_enabled", true) 50 | .put("content_driven_season_assets", true) 51 | .put("paid_earned_rubies_enabled", true) 52 | .getMap() 53 | ), EarthApiResponse.class)); 54 | 55 | this.addHandler(new Route.Builder(Request.Method.GET, "/settings").build(), request -> Response.okFromJson(new EarthApiResponse<>(new MapBuilder<>() 56 | .put("encounterinteractionradius", 40) 57 | .put("tappableinteractionradius", 70) 58 | .put("tappablevisibleradius", -5) 59 | .put("targetpossibletappables", 100) 60 | .put("tile0", 10537) 61 | .put("slowrequesttimeout", 2500) 62 | .put("cullingradius", 50) 63 | .put("commontapcount", 3) 64 | .put("epictapcount", 7) 65 | .put("speedwarningcooldown", 3600) 66 | .put("mintappablesrequiredpertile", 22) 67 | .put("targetactivetappables", 30) 68 | .put("tappablecullingradius", 500) 69 | .put("raretapcount", 5) 70 | .put("requestwarningtimeout", 10000) 71 | .put("speedwarningthreshold", 11.176f) 72 | .put("asaanchormaxplaneheightthreshold", 0.5f) 73 | .put("maxannouncementscount", 0) 74 | .put("removethislater", 23) 75 | .put("crystalslotcap", 3) 76 | .put("crystaluncommonduration", 10) 77 | .put("crystalrareduration", 10) 78 | .put("crystalepicduration", 10) 79 | .put("crystalcommonduration", 10) 80 | .put("crystallegendaryduration", 10) 81 | .put("maximumpersonaltimedchallenges", 3) 82 | .put("maximumpersonalcontinuouschallenges", 3) 83 | .getMap() 84 | ), EarthApiResponse.class)); 85 | } 86 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/utils/CraftingCalculator.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.utils; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.db.model.common.NonStackableItemInstance; 6 | import micheal65536.vienna.db.model.player.workshop.CraftingSlot; 7 | import micheal65536.vienna.db.model.player.workshop.InputItem; 8 | import micheal65536.vienna.staticdata.Catalog; 9 | 10 | import java.util.Arrays; 11 | import java.util.LinkedList; 12 | 13 | public final class CraftingCalculator 14 | { 15 | @NotNull 16 | public static State calculateState(long currentTime, @NotNull CraftingSlot.ActiveJob activeJob, @NotNull Catalog catalog) 17 | { 18 | Catalog.RecipesCatalog.CraftingRecipe recipe = catalog.recipesCatalog.getCraftingRecipe(activeJob.recipeId()); 19 | 20 | long roundDuration = recipe.duration() * 1000; 21 | int completedRounds = activeJob.finishedEarly() ? activeJob.totalRounds() : Math.min((int) ((currentTime - activeJob.startTime()) / roundDuration), activeJob.totalRounds()); 22 | int availableRounds = completedRounds - activeJob.collectedRounds(); 23 | 24 | LinkedList input = new LinkedList<>(); 25 | if (activeJob.input().length != recipe.ingredients().length) 26 | { 27 | throw new AssertionError(); 28 | } 29 | for (int index = 0; index < recipe.ingredients().length; index++) 30 | { 31 | int usedCount = recipe.ingredients()[index].count() * completedRounds; 32 | InputItem[] inputItems = activeJob.input()[index]; 33 | for (InputItem inputItem : inputItems) 34 | { 35 | if (usedCount == 0) 36 | { 37 | input.add(inputItem); 38 | } 39 | else if (usedCount > inputItem.count()) 40 | { 41 | usedCount -= inputItem.count(); 42 | } 43 | else 44 | { 45 | if (inputItem.instances().length > 0) 46 | { 47 | if (inputItem.instances().length != inputItem.count()) 48 | { 49 | throw new AssertionError(); 50 | } 51 | input.add(new InputItem(inputItem.id(), inputItem.count() - usedCount, Arrays.copyOfRange(inputItem.instances(), usedCount, inputItem.instances().length))); 52 | } 53 | else 54 | { 55 | input.add(new InputItem(inputItem.id(), inputItem.count() - usedCount, new NonStackableItemInstance[0])); 56 | } 57 | usedCount = 0; 58 | } 59 | } 60 | } 61 | 62 | return new State( 63 | completedRounds, 64 | availableRounds, 65 | activeJob.totalRounds(), 66 | input.toArray(InputItem[]::new), 67 | new State.OutputItem(recipe.output().itemId(), recipe.output().count()), 68 | activeJob.startTime() + roundDuration * (completedRounds + 1), 69 | activeJob.startTime() + roundDuration * activeJob.totalRounds(), 70 | completedRounds == activeJob.totalRounds() 71 | ); 72 | } 73 | 74 | public record State( 75 | int completedRounds, 76 | int availableRounds, 77 | int totalRounds, 78 | @NotNull InputItem[] input, 79 | @NotNull OutputItem output, 80 | long nextCompletionTime, 81 | long totalCompletionTime, 82 | boolean completed 83 | ) 84 | { 85 | public record OutputItem( 86 | @NotNull String id, 87 | int count 88 | ) 89 | { 90 | } 91 | } 92 | 93 | // TODO: make this configurable 94 | @NotNull 95 | public static FinishPrice calculateFinishPrice(int remainingTime) 96 | { 97 | if (remainingTime < 0) 98 | { 99 | throw new IllegalArgumentException(); 100 | } 101 | 102 | int periods = remainingTime / 10000; 103 | if (remainingTime % 10000 > 0) 104 | { 105 | periods = periods + 1; 106 | } 107 | int price = periods * 5; 108 | int changesAt = (periods - 1) * 10000; 109 | int validFor = remainingTime - changesAt; 110 | 111 | return new FinishPrice(price, validFor); 112 | } 113 | 114 | public record FinishPrice( 115 | int price, 116 | int validFor 117 | ) 118 | { 119 | } 120 | 121 | // TODO: make this configurable 122 | public static int calculateUnlockPrice(int slotIndex) 123 | { 124 | if (slotIndex < 1 || slotIndex > 3) 125 | { 126 | throw new IllegalArgumentException(); 127 | } 128 | return slotIndex * 5; 129 | } 130 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routes/player/JournalRouter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routes.player; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import micheal65536.vienna.apiserver.routing.Request; 6 | import micheal65536.vienna.apiserver.routing.Response; 7 | import micheal65536.vienna.apiserver.routing.Router; 8 | import micheal65536.vienna.apiserver.routing.ServerErrorException; 9 | import micheal65536.vienna.apiserver.utils.EarthApiResponse; 10 | import micheal65536.vienna.apiserver.utils.Rewards; 11 | import micheal65536.vienna.apiserver.utils.TimeFormatter; 12 | import micheal65536.vienna.db.DatabaseException; 13 | import micheal65536.vienna.db.EarthDB; 14 | import micheal65536.vienna.db.model.player.ActivityLog; 15 | import micheal65536.vienna.db.model.player.Journal; 16 | 17 | import java.util.Arrays; 18 | import java.util.Collections; 19 | import java.util.HashMap; 20 | import java.util.LinkedList; 21 | 22 | public class JournalRouter extends Router 23 | { 24 | public JournalRouter(@NotNull EarthDB earthDB) 25 | { 26 | this.addHandler(new Route.Builder(Request.Method.GET, "/player/journal").build(), request -> 27 | { 28 | String playerId = request.getContextData("playerId"); 29 | Journal journalModel; 30 | ActivityLog activityLogModel; 31 | try 32 | { 33 | EarthDB.Results results = new EarthDB.Query(false) 34 | .get("journal", playerId, Journal.class) 35 | .get("activityLog", playerId, ActivityLog.class) 36 | .execute(earthDB); 37 | journalModel = (Journal) results.get("journal").value(); 38 | activityLogModel = (ActivityLog) results.get("activityLog").value(); 39 | } 40 | catch (DatabaseException exception) 41 | { 42 | throw new ServerErrorException(exception); 43 | } 44 | 45 | HashMap inventoryJournal = new HashMap<>(); 46 | journalModel.getItems().forEach((uuid, itemJournalEntry) -> inventoryJournal.put(uuid, new micheal65536.vienna.apiserver.types.journal.Journal.InventoryJournalEntry( 47 | TimeFormatter.formatTime(itemJournalEntry.firstSeen()), 48 | TimeFormatter.formatTime(itemJournalEntry.lastSeen()), 49 | itemJournalEntry.amountCollected() 50 | ))); 51 | 52 | LinkedList activityLogList = Arrays.stream(activityLogModel.getEntries()) 53 | .map(JournalRouter::activityLogEntryToApiResponse) 54 | .collect(LinkedList::new, LinkedList::add, LinkedList::addAll); 55 | Collections.reverse(activityLogList); 56 | micheal65536.vienna.apiserver.types.journal.Journal.ActivityLogEntry[] activityLog = activityLogList.toArray(micheal65536.vienna.apiserver.types.journal.Journal.ActivityLogEntry[]::new); 57 | 58 | return Response.okFromJson(new EarthApiResponse<>(new micheal65536.vienna.apiserver.types.journal.Journal(inventoryJournal, activityLog)), EarthApiResponse.class); 59 | }); 60 | } 61 | 62 | @NotNull 63 | private static micheal65536.vienna.apiserver.types.journal.Journal.ActivityLogEntry activityLogEntryToApiResponse(@NotNull ActivityLog.Entry entry) 64 | { 65 | Rewards rewards = switch (entry.type) 66 | { 67 | case LEVEL_UP -> new Rewards().setLevel(((ActivityLog.LevelUpEntry) entry).level); 68 | case TAPPABLE -> Rewards.fromDBRewardsModel(((ActivityLog.TappableEntry) entry).rewards); 69 | case JOURNAL_ITEM_UNLOCKED -> new Rewards().addItem(((ActivityLog.JournalItemUnlockedEntry) entry).itemId, 0); 70 | case CRAFTING_COMPLETED -> Rewards.fromDBRewardsModel(((ActivityLog.CraftingCompletedEntry) entry).rewards); 71 | case SMELTING_COMPLETED -> Rewards.fromDBRewardsModel(((ActivityLog.SmeltingCompletedEntry) entry).rewards); 72 | case BOOST_ACTIVATED -> new Rewards(); 73 | }; 74 | 75 | HashMap properties = new HashMap<>(); 76 | switch (entry.type) 77 | { 78 | case BOOST_ACTIVATED -> 79 | { 80 | properties.put("boostId", ((ActivityLog.BoostActivatedEntry) entry).itemId); 81 | } 82 | } 83 | 84 | return new micheal65536.vienna.apiserver.types.journal.Journal.ActivityLogEntry( 85 | micheal65536.vienna.apiserver.types.journal.Journal.ActivityLogEntry.Type.valueOf(entry.type.name()), 86 | TimeFormatter.formatTime(entry.timestamp), 87 | rewards.toApiResponse(), 88 | properties 89 | ); 90 | } 91 | } -------------------------------------------------------------------------------- /apiserver/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | micheal65536.vienna 8 | apiserver 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | UTF-8 15 | 16 | 17 | 18 | 19 | 20 | maven-assembly-plugin 21 | 3.6.0 22 | 23 | 24 | jar-with-dependencies 25 | 26 | 27 | 28 | micheal65536.vienna.apiserver.Main 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | package 38 | 39 | single 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.apache.tomcat.embed 50 | tomcat-embed-core 51 | 10.1.19 52 | 53 | 54 | 55 | micheal65536.vienna 56 | db 57 | 0.0.1-SNAPSHOT 58 | 59 | 60 | micheal65536.vienna 61 | staticdata 62 | 0.0.1-SNAPSHOT 63 | 64 | 65 | micheal65536.vienna 66 | eventbus-client 67 | 0.0.1-SNAPSHOT 68 | 69 | 70 | micheal65536.vienna 71 | objectstore-client 72 | 0.0.1-SNAPSHOT 73 | 74 | 75 | micheal65536.vienna 76 | buildplate-connector-model 77 | 0.0.1-SNAPSHOT 78 | 79 | 80 | 81 | com.google.code.gson 82 | gson 83 | 2.10.1 84 | 85 | 86 | 87 | commons-cli 88 | commons-cli 89 | 1.6.0 90 | 91 | 92 | org.apache.logging.log4j 93 | log4j-api 94 | 2.22.1 95 | 96 | 97 | org.apache.logging.log4j 98 | log4j-core 99 | 2.22.1 100 | 101 | 102 | 103 | org.jetbrains 104 | annotations 105 | 24.1.0 106 | provided 107 | 108 | 109 | -------------------------------------------------------------------------------- /tappablesgenerator/src/main/java/micheal65536/vienna/tappablesgenerator/TappableGenerator.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.tappablesgenerator; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import micheal65536.vienna.staticdata.StaticData; 7 | import micheal65536.vienna.staticdata.TappablesConfig; 8 | 9 | import java.util.Arrays; 10 | import java.util.Comparator; 11 | import java.util.LinkedList; 12 | import java.util.Random; 13 | import java.util.UUID; 14 | 15 | public class TappableGenerator 16 | { 17 | // TODO: make these configurable 18 | private static final int MIN_COUNT = 1; 19 | private static final int MAX_COUNT = 3; 20 | private static final long MIN_DURATION = 2 * 60 * 1000; 21 | private static final long MAX_DURATION = 5 * 60 * 1000; 22 | private static final long MIN_DELAY = 1 * 60 * 1000; 23 | private static final long MAX_DELAY = 2 * 60 * 1000; 24 | 25 | private final StaticData staticData; 26 | 27 | private final Random random; 28 | 29 | public TappableGenerator(@NotNull StaticData staticData) 30 | { 31 | this.staticData = staticData; 32 | if (this.staticData.tappablesConfig.tappables.length == 0) 33 | { 34 | LogManager.getLogger().warn("No tappable configs provided"); 35 | } 36 | 37 | this.random = new Random(); 38 | } 39 | 40 | public long getMaxTappableLifetime() 41 | { 42 | return MAX_DELAY + MAX_DURATION + 30 * 1000; 43 | } 44 | 45 | @NotNull 46 | public Tappable[] generateTappables(int tileX, int tileY, long currentTime) 47 | { 48 | if (this.staticData.tappablesConfig.tappables.length == 0) 49 | { 50 | return new Tappable[0]; 51 | } 52 | 53 | LinkedList tappables = new LinkedList<>(); 54 | for (int count = this.random.nextInt(MIN_COUNT, MAX_COUNT + 1); count > 0; count--) 55 | { 56 | long spawnDelay = this.random.nextLong(MIN_DELAY, MAX_DELAY + 1); 57 | long duration = this.random.nextLong(MIN_DURATION, MAX_DURATION + 1); 58 | 59 | TappablesConfig.TappableConfig tappableConfig = this.staticData.tappablesConfig.tappables[this.random.nextInt(0, this.staticData.tappablesConfig.tappables.length)]; 60 | 61 | float[] tileBounds = getTileBounds(tileX, tileY); 62 | float lat = this.random.nextFloat(tileBounds[1], tileBounds[0]); 63 | float lon = this.random.nextFloat(tileBounds[2], tileBounds[3]); 64 | 65 | int dropSetIndex = this.random.nextInt(0, Arrays.stream(tappableConfig.dropSets()).mapToInt(dropSet -> dropSet.chance()).sum()); 66 | TappablesConfig.TappableConfig.DropSet dropSet = null; 67 | for (TappablesConfig.TappableConfig.DropSet dropSet1 : tappableConfig.dropSets()) 68 | { 69 | dropSet = dropSet1; 70 | dropSetIndex -= dropSet1.chance(); 71 | if (dropSetIndex <= 0) 72 | { 73 | break; 74 | } 75 | } 76 | if (dropSet == null) 77 | { 78 | throw new AssertionError(); 79 | } 80 | 81 | LinkedList items = new LinkedList<>(); 82 | for (String itemId : dropSet.items()) 83 | { 84 | TappablesConfig.TappableConfig.ItemCount itemCount = tappableConfig.itemCounts().get(itemId); 85 | items.add(new Tappable.Item(itemId, this.random.nextInt(itemCount.min(), itemCount.max() + 1))); 86 | } 87 | 88 | Tappable.Rarity rarity = items.stream().map(item -> this.staticData.catalog.itemsCatalog.getItem(item.id()).rarity()).max(Comparator.comparing(rarity1 -> rarity1.ordinal())).map(rarity1 -> Tappable.Rarity.valueOf(rarity1.name())).orElseThrow(); 89 | 90 | Tappable tappable = new Tappable( 91 | UUID.randomUUID().toString(), 92 | lat, 93 | lon, 94 | currentTime + spawnDelay, 95 | duration, 96 | tappableConfig.icon(), 97 | rarity, 98 | items.toArray(Tappable.Item[]::new) 99 | ); 100 | tappables.add(tappable); 101 | } 102 | return tappables.toArray(Tappable[]::new); 103 | } 104 | 105 | private static float[] getTileBounds(int tileX, int tileY) 106 | { 107 | return new float[]{ 108 | yToLat((float) tileY / (1 << 16)), 109 | yToLat((float) (tileY + 1) / (1 << 16)), 110 | xToLon((float) tileX / (1 << 16)), 111 | xToLon((float) (tileX + 1) / (1 << 16)) 112 | }; 113 | } 114 | 115 | private static float xToLon(float x) 116 | { 117 | return (float) Math.toDegrees((x * 2.0 - 1.0) * Math.PI); 118 | } 119 | 120 | private static float yToLat(float y) 121 | { 122 | return (float) Math.toDegrees(Math.atan(Math.sinh((1.0 - y * 2.0) * Math.PI))); 123 | } 124 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routes/player/TokensRouter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routes.player; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import micheal65536.vienna.apiserver.routing.Request; 7 | import micheal65536.vienna.apiserver.routing.Response; 8 | import micheal65536.vienna.apiserver.routing.Router; 9 | import micheal65536.vienna.apiserver.routing.ServerErrorException; 10 | import micheal65536.vienna.apiserver.types.common.Token; 11 | import micheal65536.vienna.apiserver.utils.EarthApiResponse; 12 | import micheal65536.vienna.apiserver.utils.MapBuilder; 13 | import micheal65536.vienna.apiserver.utils.Rewards; 14 | import micheal65536.vienna.apiserver.utils.TokenUtils; 15 | import micheal65536.vienna.db.DatabaseException; 16 | import micheal65536.vienna.db.EarthDB; 17 | import micheal65536.vienna.db.model.player.Tokens; 18 | import micheal65536.vienna.staticdata.StaticData; 19 | 20 | import java.util.Arrays; 21 | import java.util.HashMap; 22 | 23 | public class TokensRouter extends Router 24 | { 25 | public TokensRouter(@NotNull EarthDB earthDB, @NotNull StaticData staticData) 26 | { 27 | this.addHandler(new Route.Builder(Request.Method.GET, "/player/tokens").build(), request -> 28 | { 29 | try 30 | { 31 | Tokens tokens = (Tokens) new EarthDB.Query(false) 32 | .get("tokens", request.getContextData("playerId"), Tokens.class) 33 | .execute(earthDB) 34 | .get("tokens").value(); 35 | return Response.okFromJson(new EarthApiResponse<>( 36 | new MapBuilder<>().put("tokens", 37 | Arrays.stream(tokens.getTokens()).collect(HashMap::new, (hashMap, token) -> 38 | { 39 | hashMap.put(token.id(), tokenToApiResponse(token.token())); 40 | }, HashMap::putAll) 41 | ).getMap() 42 | ), EarthApiResponse.class); 43 | } 44 | catch (DatabaseException exception) 45 | { 46 | LogManager.getLogger().error(exception); 47 | return Response.serverError(); 48 | } 49 | }); 50 | 51 | this.addHandler(new Route.Builder(Request.Method.POST, "/player/tokens/$tokenId/redeem").build(), request -> 52 | { 53 | Tokens.Token token; 54 | try 55 | { 56 | String playerId = request.getContextData("playerId"); 57 | String tokenId = request.getParameter("tokenId"); 58 | EarthDB.Results results = new EarthDB.Query(true) 59 | .get("tokens", playerId, Tokens.class) 60 | .then(results1 -> 61 | { 62 | Tokens tokens = (Tokens) results1.get("tokens").value(); 63 | Tokens.Token removedToken = tokens.removeToken(tokenId); 64 | if (removedToken != null) 65 | { 66 | return new EarthDB.Query(true) 67 | .update("tokens", playerId, tokens) 68 | .then(TokenUtils.doActionsOnRedeemedToken(removedToken, playerId, request.timestamp, staticData), false) 69 | .extra("success", true) 70 | .extra("token", removedToken); 71 | } 72 | else 73 | { 74 | return new EarthDB.Query(false) 75 | .extra("success", false); 76 | } 77 | }) 78 | .execute(earthDB); 79 | token = (boolean) results.getExtra("success") ? (Tokens.Token) results.getExtra("token") : null; 80 | } 81 | catch (DatabaseException exception) 82 | { 83 | throw new ServerErrorException(exception); 84 | } 85 | 86 | if (token != null) 87 | { 88 | return Response.okFromJson(tokenToApiResponse(token), Token.class); 89 | } 90 | else 91 | { 92 | return Response.badRequest(); 93 | } 94 | }); 95 | } 96 | 97 | @NotNull 98 | private static Token tokenToApiResponse(@NotNull Tokens.Token token) 99 | { 100 | HashMap properties = new HashMap<>(); 101 | switch (token.type) 102 | { 103 | case JOURNAL_ITEM_UNLOCKED -> 104 | { 105 | properties.put("itemid", ((Tokens.JournalItemUnlockedToken) token).itemId); 106 | } 107 | } 108 | 109 | Rewards rewards = switch (token.type) 110 | { 111 | case LEVEL_UP -> Rewards.fromDBRewardsModel(((Tokens.LevelUpToken) token).rewards).setLevel(((Tokens.LevelUpToken) token).level); 112 | default -> new Rewards(); 113 | }; 114 | 115 | Token.Lifetime lifetime = switch (token.type) 116 | { 117 | case LEVEL_UP -> Token.Lifetime.TRANSIENT; 118 | case JOURNAL_ITEM_UNLOCKED -> Token.Lifetime.PERSISTENT; 119 | }; 120 | 121 | return new Token( 122 | Token.Type.valueOf(token.type.name()), 123 | properties, 124 | rewards.toApiResponse(), 125 | lifetime 126 | ); 127 | } 128 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/ActivityLog.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | import com.google.gson.JsonDeserializationContext; 4 | import com.google.gson.JsonDeserializer; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonParseException; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import micheal65536.vienna.db.model.common.Rewards; 10 | 11 | import java.util.LinkedList; 12 | 13 | public final class ActivityLog 14 | { 15 | @NotNull 16 | private final LinkedList entries; 17 | 18 | public ActivityLog() 19 | { 20 | this.entries = new LinkedList<>(); 21 | } 22 | 23 | @NotNull 24 | public ActivityLog copy() 25 | { 26 | ActivityLog activityLog = new ActivityLog(); 27 | activityLog.entries.addAll(this.entries); 28 | return activityLog; 29 | } 30 | 31 | @NotNull 32 | public Entry[] getEntries() 33 | { 34 | return this.entries.toArray(Entry[]::new); 35 | } 36 | 37 | public void addEntry(@NotNull Entry entry) 38 | { 39 | this.entries.add(entry); 40 | } 41 | 42 | public void prune() 43 | { 44 | // it is widely known that the activity log is length limited but there is only ONE person who has stated how long it was limited to and apparently it is 40 entires 45 | while (this.entries.size() > 40) 46 | { 47 | this.entries.removeFirst(); 48 | } 49 | } 50 | 51 | public static abstract class Entry 52 | { 53 | public final long timestamp; 54 | @NotNull 55 | public final Type type; 56 | 57 | private Entry(long timestamp, @NotNull Type type) 58 | { 59 | this.timestamp = timestamp; 60 | this.type = type; 61 | } 62 | 63 | public enum Type 64 | { 65 | LEVEL_UP, 66 | TAPPABLE, 67 | JOURNAL_ITEM_UNLOCKED, 68 | CRAFTING_COMPLETED, 69 | SMELTING_COMPLETED, 70 | BOOST_ACTIVATED 71 | } 72 | 73 | public static class Deserializer implements JsonDeserializer 74 | { 75 | private static class BaseEntry extends Entry 76 | { 77 | private BaseEntry(long timestamp, @NotNull Type type) 78 | { 79 | super(timestamp, type); 80 | } 81 | } 82 | 83 | @Override 84 | public Entry deserialize(JsonElement jsonElement, java.lang.reflect.Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException 85 | { 86 | BaseEntry baseEntry = jsonDeserializationContext.deserialize(jsonElement, BaseEntry.class); 87 | return jsonDeserializationContext.deserialize(jsonElement, switch (baseEntry.type) 88 | { 89 | case LEVEL_UP -> LevelUpEntry.class; 90 | case TAPPABLE -> TappableEntry.class; 91 | case JOURNAL_ITEM_UNLOCKED -> JournalItemUnlockedEntry.class; 92 | case CRAFTING_COMPLETED -> CraftingCompletedEntry.class; 93 | case SMELTING_COMPLETED -> SmeltingCompletedEntry.class; 94 | case BOOST_ACTIVATED -> BoostActivatedEntry.class; 95 | }); 96 | } 97 | } 98 | } 99 | 100 | public static final class LevelUpEntry extends Entry 101 | { 102 | public final int level; 103 | 104 | public LevelUpEntry(long timestamp, int level) 105 | { 106 | super(timestamp, Type.LEVEL_UP); 107 | this.level = level; 108 | } 109 | } 110 | 111 | public static final class TappableEntry extends Entry 112 | { 113 | @NotNull 114 | public final Rewards rewards; 115 | 116 | public TappableEntry(long timestamp, @NotNull Rewards rewards) 117 | { 118 | super(timestamp, Type.TAPPABLE); 119 | this.rewards = rewards; 120 | } 121 | } 122 | 123 | public static final class JournalItemUnlockedEntry extends Entry 124 | { 125 | @NotNull 126 | public final String itemId; 127 | 128 | public JournalItemUnlockedEntry(long timestamp, @NotNull String itemId) 129 | { 130 | super(timestamp, Type.JOURNAL_ITEM_UNLOCKED); 131 | this.itemId = itemId; 132 | } 133 | } 134 | 135 | public static final class CraftingCompletedEntry extends Entry 136 | { 137 | @NotNull 138 | public final Rewards rewards; 139 | 140 | public CraftingCompletedEntry(long timestamp, @NotNull Rewards rewards) 141 | { 142 | super(timestamp, Type.CRAFTING_COMPLETED); 143 | this.rewards = rewards; 144 | } 145 | } 146 | 147 | public static final class SmeltingCompletedEntry extends Entry 148 | { 149 | @NotNull 150 | public final Rewards rewards; 151 | 152 | public SmeltingCompletedEntry(long timestamp, @NotNull Rewards rewards) 153 | { 154 | super(timestamp, Type.SMELTING_COMPLETED); 155 | this.rewards = rewards; 156 | } 157 | } 158 | 159 | public static final class BoostActivatedEntry extends Entry 160 | { 161 | @NotNull 162 | public final String itemId; 163 | 164 | public BoostActivatedEntry(long timestamp, @NotNull String itemId) 165 | { 166 | super(timestamp, Type.BOOST_ACTIVATED); 167 | this.itemId = itemId; 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /utils/http-routing/src/main/java/micheal65536/vienna/utils/http/routing/Request.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.utils.http.routing; 2 | 3 | import com.google.gson.Gson; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.HashMap; 8 | import java.util.function.Function; 9 | 10 | public final class Request 11 | { 12 | public final long timestamp; 13 | @NotNull 14 | public final Path path; 15 | @NotNull 16 | public final Method method; 17 | final HashMap parameters = new HashMap<>(); 18 | final HashMap contextData = new HashMap<>(); 19 | private final String body; 20 | 21 | Request(long timestamp, @NotNull Path path, @NotNull Method method, @NotNull String body) 22 | { 23 | this.timestamp = timestamp; 24 | this.path = path; 25 | this.method = method; 26 | this.body = body; 27 | } 28 | 29 | public void addContextData(@NotNull String name, @NotNull T value) 30 | { 31 | this.contextData.put(name, value); 32 | } 33 | 34 | @NotNull 35 | public String getParameter(@NotNull String name) throws BadRequestException 36 | { 37 | return this.getParameter(name, true, null); 38 | } 39 | 40 | public String getParameter(@NotNull String name, @Nullable String defaultValue) throws BadRequestException 41 | { 42 | return this.getParameter(name, false, defaultValue); 43 | } 44 | 45 | public int getParameterInt(@NotNull String name) throws BadRequestException 46 | { 47 | return this.getConvertedParameter(name, true, 0, Integer::parseInt); 48 | } 49 | 50 | public int getParameterInt(@NotNull String name, int defaultValue) throws BadRequestException 51 | { 52 | return this.getConvertedParameter(name, false, defaultValue, Integer::parseInt); 53 | } 54 | 55 | public float getParameterFloat(@NotNull String name) throws BadRequestException 56 | { 57 | return this.getConvertedParameter(name, true, 0.0f, Float::parseFloat); 58 | } 59 | 60 | public float getParameterFloat(@NotNull String name, float defaultValue) throws BadRequestException 61 | { 62 | return this.getConvertedParameter(name, false, defaultValue, Float::parseFloat); 63 | } 64 | 65 | public boolean getParameterBooleanByPresence(@NotNull String name) throws BadRequestException 66 | { 67 | return this.parameters.containsKey(name); 68 | } 69 | 70 | @NotNull 71 | public T getContextData(@NotNull String name) throws BadRequestException 72 | { 73 | T data = (T) this.contextData.getOrDefault(name, null); 74 | if (data == null) 75 | { 76 | throw new BadRequestException("Missing required context data"); 77 | } 78 | return data; 79 | } 80 | 81 | @NotNull 82 | public String getBody() throws BadRequestException 83 | { 84 | return this.body; 85 | } 86 | 87 | @NotNull 88 | public T getBodyAsJson(@NotNull Class tClass) throws BadRequestException 89 | { 90 | try 91 | { 92 | return new Gson().fromJson(this.getBody(), tClass); 93 | } 94 | catch (Exception exception) 95 | { 96 | throw new BadRequestException(exception); 97 | } 98 | } 99 | 100 | private String getParameter(String name, boolean required, String defaultValue) throws BadRequestException 101 | { 102 | String parameter = this.parameters.getOrDefault(name, null); 103 | if (parameter == null) 104 | { 105 | if (required) 106 | { 107 | throw new BadRequestException("Missing required parameter %s".formatted(name)); 108 | } 109 | else 110 | { 111 | return defaultValue; 112 | } 113 | } 114 | return parameter; 115 | } 116 | 117 | private T getConvertedParameter(String name, boolean required, T defaultValue, Function converter) throws BadRequestException 118 | { 119 | String parameter = this.getParameter(name, required, null); 120 | if (parameter == null) 121 | { 122 | return defaultValue; 123 | } 124 | try 125 | { 126 | T converted = converter.apply(parameter); 127 | if (converted == null) 128 | { 129 | throw new BadRequestException("Could not convert parameter %s to requested type".formatted(name)); 130 | } 131 | return converted; 132 | } 133 | catch (Exception exception) 134 | { 135 | throw new BadRequestException("Could not convert parameter %s to requested type".formatted(name), exception); 136 | } 137 | } 138 | 139 | public enum Method 140 | { 141 | HEAD, 142 | GET, 143 | POST, 144 | PUT 145 | } 146 | 147 | public static final class BadRequestException extends Exception 148 | { 149 | private BadRequestException(String message) 150 | { 151 | super(message); 152 | } 153 | 154 | private BadRequestException(String message, Throwable cause) 155 | { 156 | super(message, cause); 157 | } 158 | 159 | private BadRequestException(Throwable cause) 160 | { 161 | super(cause); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routing/Request.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routing; 2 | 3 | import com.google.gson.Gson; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.HashMap; 8 | import java.util.function.Function; 9 | 10 | public final class Request 11 | { 12 | public final long timestamp; 13 | @NotNull 14 | public final Path path; 15 | @NotNull 16 | public final Method method; 17 | final HashMap parameters = new HashMap<>(); 18 | final HashMap contextData = new HashMap<>(); 19 | private final String body; 20 | 21 | Request(long timestamp, @NotNull Path path, @NotNull Method method, @NotNull String body) 22 | { 23 | this.timestamp = timestamp; 24 | this.path = path; 25 | this.method = method; 26 | this.body = body; 27 | } 28 | 29 | public void addContextData(@NotNull String name, @NotNull T value) 30 | { 31 | this.contextData.put(name, value); 32 | } 33 | 34 | @NotNull 35 | public String getParameter(@NotNull String name) throws BadRequestException 36 | { 37 | return this.getParameter(name, true, null); 38 | } 39 | 40 | public String getParameter(@NotNull String name, @Nullable String defaultValue) throws BadRequestException 41 | { 42 | return this.getParameter(name, false, defaultValue); 43 | } 44 | 45 | public int getParameterInt(@NotNull String name) throws BadRequestException 46 | { 47 | return this.getConvertedParameter(name, true, 0, Integer::parseInt); 48 | } 49 | 50 | public int getParameterInt(@NotNull String name, int defaultValue) throws BadRequestException 51 | { 52 | return this.getConvertedParameter(name, false, defaultValue, Integer::parseInt); 53 | } 54 | 55 | public float getParameterFloat(@NotNull String name) throws BadRequestException 56 | { 57 | return this.getConvertedParameter(name, true, 0.0f, Float::parseFloat); 58 | } 59 | 60 | public float getParameterFloat(@NotNull String name, float defaultValue) throws BadRequestException 61 | { 62 | return this.getConvertedParameter(name, false, defaultValue, Float::parseFloat); 63 | } 64 | 65 | public boolean getParameterBooleanByPresence(@NotNull String name) throws BadRequestException 66 | { 67 | return this.parameters.containsKey(name); 68 | } 69 | 70 | @NotNull 71 | public T getContextData(@NotNull String name) throws BadRequestException 72 | { 73 | T data = (T) this.contextData.getOrDefault(name, null); 74 | if (data == null) 75 | { 76 | throw new BadRequestException("Missing required context data"); 77 | } 78 | return data; 79 | } 80 | 81 | @NotNull 82 | public String getBody() throws BadRequestException 83 | { 84 | return this.body; 85 | } 86 | 87 | @NotNull 88 | public T getBodyAsJson(@NotNull Class tClass) throws BadRequestException 89 | { 90 | try 91 | { 92 | return new Gson().fromJson(this.getBody(), tClass); 93 | } 94 | catch (Exception exception) 95 | { 96 | throw new BadRequestException(exception); 97 | } 98 | } 99 | 100 | private String getParameter(String name, boolean required, String defaultValue) throws BadRequestException 101 | { 102 | String parameter = this.parameters.getOrDefault(name, null); 103 | if (parameter == null) 104 | { 105 | if (required) 106 | { 107 | throw new BadRequestException("Missing required parameter %s".formatted(name)); 108 | } 109 | else 110 | { 111 | return defaultValue; 112 | } 113 | } 114 | return parameter; 115 | } 116 | 117 | private T getConvertedParameter(String name, boolean required, T defaultValue, Function converter) throws BadRequestException 118 | { 119 | String parameter = this.getParameter(name, required, null); 120 | if (parameter == null) 121 | { 122 | return defaultValue; 123 | } 124 | try 125 | { 126 | T converted = converter.apply(parameter); 127 | if (converted == null) 128 | { 129 | throw new BadRequestException("Could not convert parameter %s to requested type".formatted(name)); 130 | } 131 | return converted; 132 | } 133 | catch (Exception exception) 134 | { 135 | throw new BadRequestException("Could not convert parameter %s to requested type".formatted(name), exception); 136 | } 137 | } 138 | 139 | public enum Method 140 | { 141 | HEAD, 142 | GET, 143 | POST, 144 | PUT, 145 | DELETE 146 | } 147 | 148 | public static final class BadRequestException extends Exception 149 | { 150 | private BadRequestException(String message) 151 | { 152 | super(message); 153 | } 154 | 155 | private BadRequestException(String message, Throwable cause) 156 | { 157 | super(message, cause); 158 | } 159 | 160 | private BadRequestException(Throwable cause) 161 | { 162 | super(cause); 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /db/src/main/java/micheal65536/vienna/db/model/player/Inventory.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.db.model.player; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import micheal65536.vienna.db.model.common.NonStackableItemInstance; 7 | 8 | import java.util.HashMap; 9 | import java.util.LinkedList; 10 | 11 | public final class Inventory 12 | { 13 | @NotNull 14 | private final HashMap stackableItems; 15 | @NotNull 16 | private final HashMap> nonStackableItems; 17 | 18 | public Inventory() 19 | { 20 | this.stackableItems = new HashMap<>(); 21 | this.nonStackableItems = new HashMap<>(); 22 | } 23 | 24 | @NotNull 25 | public Inventory copy() 26 | { 27 | Inventory inventory = new Inventory(); 28 | inventory.stackableItems.putAll(this.stackableItems); 29 | HashMap> nonStackableItems = new HashMap<>(); 30 | this.nonStackableItems.forEach((id, instances) -> nonStackableItems.put(id, new HashMap<>(instances))); 31 | inventory.nonStackableItems.putAll(nonStackableItems); 32 | return inventory; 33 | } 34 | 35 | public record StackableItem( 36 | @NotNull String id, 37 | int count 38 | ) 39 | { 40 | } 41 | 42 | @NotNull 43 | public StackableItem[] getStackableItems() 44 | { 45 | return this.stackableItems.entrySet().stream().map(entry -> new StackableItem(entry.getKey(), entry.getValue())).toArray(StackableItem[]::new); 46 | } 47 | 48 | public record NonStackableItem( 49 | @NotNull String id, 50 | @NotNull NonStackableItemInstance[] instances 51 | ) 52 | { 53 | } 54 | 55 | @NotNull 56 | public NonStackableItem[] getNonStackableItems() 57 | { 58 | return this.nonStackableItems.entrySet().stream().map(entry -> new NonStackableItem(entry.getKey(), entry.getValue().values().toArray(NonStackableItemInstance[]::new))).toArray(NonStackableItem[]::new); 59 | } 60 | 61 | public int getItemCount(@NotNull String id) 62 | { 63 | Integer count = this.stackableItems.getOrDefault(id, null); 64 | if (count != null) 65 | { 66 | return count; 67 | } 68 | HashMap instances = this.nonStackableItems.getOrDefault(id, null); 69 | if (instances != null) 70 | { 71 | return instances.size(); 72 | } 73 | return 0; 74 | } 75 | 76 | @NotNull 77 | public NonStackableItemInstance[] getItemInstances(@NotNull String id) 78 | { 79 | HashMap instances = this.nonStackableItems.getOrDefault(id, null); 80 | if (instances != null) 81 | { 82 | return instances.values().toArray(NonStackableItemInstance[]::new); 83 | } 84 | return new NonStackableItemInstance[0]; 85 | } 86 | 87 | @Nullable 88 | public NonStackableItemInstance getItemInstance(@NotNull String id, @NotNull String instanceId) 89 | { 90 | HashMap instances = this.nonStackableItems.getOrDefault(id, null); 91 | if (instances != null) 92 | { 93 | return instances.getOrDefault(instanceId, null); 94 | } 95 | return null; 96 | } 97 | 98 | public void addItems(@NotNull String id, int count) 99 | { 100 | if (count < 0) 101 | { 102 | throw new IllegalArgumentException(); 103 | } 104 | this.stackableItems.put(id, this.stackableItems.getOrDefault(id, 0) + count); 105 | } 106 | 107 | public void addItems(@NotNull String id, @NotNull NonStackableItemInstance[] instances) 108 | { 109 | HashMap instancesMap = this.nonStackableItems.computeIfAbsent(id, id1 -> new HashMap<>()); 110 | for (NonStackableItemInstance instance : instances) 111 | { 112 | instancesMap.put(instance.instanceId(), instance); 113 | } 114 | } 115 | 116 | public boolean takeItems(@NotNull String id, int count) 117 | { 118 | if (count < 0) 119 | { 120 | throw new IllegalArgumentException(); 121 | } 122 | int currentCount = this.stackableItems.getOrDefault(id, 0); 123 | if (currentCount < count) 124 | { 125 | return false; 126 | } 127 | this.stackableItems.put(id, currentCount - count); 128 | return true; 129 | } 130 | 131 | public NonStackableItemInstance[] takeItems(@NotNull String id, @NotNull String[] instanceIds) 132 | { 133 | HashMap instanceMap = this.nonStackableItems.getOrDefault(id, null); 134 | if (instanceMap == null) 135 | { 136 | return null; 137 | } 138 | LinkedList instances = new LinkedList<>(); 139 | for (String instanceId : instanceIds) 140 | { 141 | NonStackableItemInstance instance = instanceMap.remove(instanceId); 142 | if (instance == null) 143 | { 144 | return null; 145 | } 146 | instances.add(instance); 147 | } 148 | return instances.toArray(NonStackableItemInstance[]::new); 149 | } 150 | } -------------------------------------------------------------------------------- /eventbus/client/src/main/java/micheal65536/vienna/eventbus/client/Publisher.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.eventbus.client; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.LinkedList; 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | 9 | public final class Publisher 10 | { 11 | private final EventBusClient client; 12 | private final int channelId; 13 | 14 | private final ReentrantLock lock = new ReentrantLock(true); 15 | 16 | private boolean closed = false; 17 | 18 | private final LinkedList queuedEvents = new LinkedList<>(); 19 | private final LinkedList> queuedEventResults = new LinkedList<>(); 20 | private CompletableFuture currentPendingEventResult = null; 21 | 22 | Publisher(@NotNull EventBusClient client, int channelId) 23 | { 24 | this.client = client; 25 | this.channelId = channelId; 26 | } 27 | 28 | public void close() 29 | { 30 | this.client.removePublisher(this.channelId); 31 | this.client.sendMessage(this.channelId, "CLOSE"); 32 | this.closed(); 33 | } 34 | 35 | public CompletableFuture publish(@NotNull String queueName, @NotNull String type, @NotNull String data) 36 | { 37 | if (!validateQueueName(queueName)) 38 | { 39 | throw new IllegalArgumentException("Queue name contains invalid characters"); 40 | } 41 | if (!validateType(type)) 42 | { 43 | throw new IllegalArgumentException("Type contains invalid characters"); 44 | } 45 | if (!validateData(data)) 46 | { 47 | throw new IllegalArgumentException("Data contains invalid characters"); 48 | } 49 | 50 | String eventMessage = "SEND " + queueName + ":" + type + ":" + data; 51 | 52 | CompletableFuture completableFuture = new CompletableFuture<>(); 53 | 54 | this.lock.lock(); 55 | if (this.closed) 56 | { 57 | completableFuture.complete(false); 58 | } 59 | else 60 | { 61 | this.queuedEvents.add(eventMessage); 62 | this.queuedEventResults.add(completableFuture); 63 | if (this.currentPendingEventResult == null) 64 | { 65 | this.sendNextEvent(); 66 | } 67 | } 68 | this.lock.unlock(); 69 | 70 | return completableFuture; 71 | } 72 | 73 | public void flush() 74 | { 75 | this.lock.lock(); 76 | CompletableFuture completableFuture = this.queuedEventResults.isEmpty() ? this.currentPendingEventResult : this.queuedEventResults.getLast(); 77 | this.lock.unlock(); 78 | 79 | if (completableFuture != null) 80 | { 81 | completableFuture.join(); 82 | } 83 | } 84 | 85 | boolean handleMessage(@NotNull String message) 86 | { 87 | if (message.equals("ACK")) 88 | { 89 | try 90 | { 91 | this.lock.lock(); 92 | if (this.currentPendingEventResult != null) 93 | { 94 | this.currentPendingEventResult.complete(true); 95 | this.currentPendingEventResult = null; 96 | if (!this.queuedEvents.isEmpty()) 97 | { 98 | this.sendNextEvent(); 99 | } 100 | return true; 101 | } 102 | else 103 | { 104 | return false; 105 | } 106 | } 107 | finally 108 | { 109 | this.lock.unlock(); 110 | } 111 | } 112 | else if (message.equals("ERR")) 113 | { 114 | this.close(); 115 | return true; 116 | } 117 | else 118 | { 119 | return false; 120 | } 121 | } 122 | 123 | private void sendNextEvent() 124 | { 125 | String message = this.queuedEvents.removeFirst(); 126 | this.client.sendMessage(this.channelId, message); 127 | this.currentPendingEventResult = this.queuedEventResults.removeFirst(); 128 | } 129 | 130 | void closed() 131 | { 132 | this.lock.lock(); 133 | 134 | this.closed = true; 135 | 136 | if (this.currentPendingEventResult != null) 137 | { 138 | this.currentPendingEventResult.complete(false); 139 | this.currentPendingEventResult = null; 140 | } 141 | this.queuedEventResults.forEach(completableFuture -> completableFuture.complete(false)); 142 | this.queuedEventResults.clear(); 143 | this.queuedEvents.clear(); 144 | 145 | this.lock.unlock(); 146 | } 147 | 148 | private static boolean validateQueueName(String queueName) 149 | { 150 | if (queueName.chars().anyMatch(character -> character < 32 || character >= 127) || queueName.isEmpty() || queueName.matches("[^A-Za-z0-9_\\-]") || queueName.matches("^[^A-Za-z0-9]")) 151 | { 152 | return false; 153 | } 154 | return true; 155 | } 156 | 157 | private static boolean validateType(String type) 158 | { 159 | if (type.chars().anyMatch(character -> character < 32 || character >= 127) || type.isEmpty() || type.matches("[^A-Za-z0-9_\\-]") || type.matches("^[^A-Za-z0-9]")) 160 | { 161 | return false; 162 | } 163 | return true; 164 | } 165 | 166 | private static boolean validateData(@NotNull String string) 167 | { 168 | return string.chars().noneMatch(character -> character < 32 || character >= 127); 169 | } 170 | } -------------------------------------------------------------------------------- /apiserver/src/main/java/micheal65536/vienna/apiserver/routes/player/ProfileRouter.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.apiserver.routes.player; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import micheal65536.vienna.apiserver.routing.Request; 7 | import micheal65536.vienna.apiserver.routing.Response; 8 | import micheal65536.vienna.apiserver.routing.Router; 9 | import micheal65536.vienna.apiserver.routing.ServerErrorException; 10 | import micheal65536.vienna.apiserver.types.profile.SplitRubies; 11 | import micheal65536.vienna.apiserver.utils.BoostUtils; 12 | import micheal65536.vienna.apiserver.utils.EarthApiResponse; 13 | import micheal65536.vienna.apiserver.utils.LevelUtils; 14 | import micheal65536.vienna.db.DatabaseException; 15 | import micheal65536.vienna.db.EarthDB; 16 | import micheal65536.vienna.db.model.player.Boosts; 17 | import micheal65536.vienna.db.model.player.Profile; 18 | import micheal65536.vienna.staticdata.Levels; 19 | import micheal65536.vienna.staticdata.StaticData; 20 | 21 | import java.util.HashMap; 22 | import java.util.Locale; 23 | import java.util.stream.IntStream; 24 | 25 | public class ProfileRouter extends Router 26 | { 27 | public ProfileRouter(@NotNull EarthDB earthDB, @NotNull StaticData staticData) 28 | { 29 | this.addHandler(new Route.Builder(Request.Method.GET, "/player/profile/$userId").build(), request -> 30 | { 31 | // TODO: decide if we should allow requests for profiles of other players 32 | String userId = request.getParameter("userId").toLowerCase(Locale.ROOT); 33 | 34 | try 35 | { 36 | EarthDB.Results results = new EarthDB.Query(false) 37 | .get("profile", userId, Profile.class) 38 | .get("boosts", userId, Boosts.class) 39 | .execute(earthDB); 40 | 41 | Profile profile = (Profile) results.get("profile").value(); 42 | Boosts boosts = (Boosts) results.get("boosts").value(); 43 | 44 | Levels.Level[] levels = staticData.levels.levels; 45 | int currentLevelExperience = profile.experience - (profile.level > 1 ? (profile.level - 2 < levels.length ? levels[profile.level - 2].experienceRequired() : levels[levels.length - 1].experienceRequired()) : 0); 46 | int experienceRemaining = profile.level - 1 < levels.length ? levels[profile.level - 1].experienceRequired() - profile.experience : 0; 47 | 48 | int maxPlayerHealth = BoostUtils.getMaxPlayerHealth(boosts, request.timestamp, staticData.catalog.itemsCatalog); 49 | if (profile.health > maxPlayerHealth) 50 | { 51 | profile.health = maxPlayerHealth; 52 | } 53 | 54 | return Response.okFromJson(new EarthApiResponse<>(new micheal65536.vienna.apiserver.types.profile.Profile( 55 | IntStream.range(0, levels.length).collect(HashMap::new, (hashMap, levelIndex) -> 56 | { 57 | Levels.Level level = levels[levelIndex]; 58 | hashMap.put(levelIndex + 1, new micheal65536.vienna.apiserver.types.profile.Profile.Level(level.experienceRequired(), LevelUtils.makeLevelRewards(level).toApiResponse())); 59 | }, HashMap::putAll), 60 | profile.experience, 61 | profile.level, 62 | currentLevelExperience, 63 | experienceRemaining, 64 | profile.health, 65 | ((float) profile.health / (float) maxPlayerHealth) * 100.0f 66 | )), EarthApiResponse.class); 67 | } 68 | catch (DatabaseException exception) 69 | { 70 | throw new ServerErrorException(exception); 71 | } 72 | }); 73 | 74 | this.addHandler(new Route.Builder(Request.Method.GET, "/player/rubies").build(), request -> 75 | { 76 | try 77 | { 78 | Profile profile = (Profile) new EarthDB.Query(false) 79 | .get("profile", request.getContextData("playerId"), Profile.class) 80 | .execute(earthDB) 81 | .get("profile").value(); 82 | return Response.okFromJson(new EarthApiResponse<>(profile.rubies.purchased + profile.rubies.earned), EarthApiResponse.class); 83 | } 84 | catch (DatabaseException exception) 85 | { 86 | LogManager.getLogger().error(exception); 87 | return Response.serverError(); 88 | } 89 | }); 90 | this.addHandler(new Route.Builder(Request.Method.GET, "/player/splitRubies").build(), request -> 91 | { 92 | try 93 | { 94 | Profile profile = (Profile) new EarthDB.Query(false) 95 | .get("profile", request.getContextData("playerId"), Profile.class) 96 | .execute(earthDB) 97 | .get("profile").value(); 98 | return Response.okFromJson(new EarthApiResponse<>(new SplitRubies(profile.rubies.purchased, profile.rubies.earned)), EarthApiResponse.class); 99 | } 100 | catch (DatabaseException exception) 101 | { 102 | LogManager.getLogger().error(exception); 103 | return Response.serverError(); 104 | } 105 | }); 106 | 107 | // required for the language selection option in the client to work 108 | this.addHandler(new Route.Builder(Request.Method.POST, "/player/profile/language").build(), request -> 109 | { 110 | return Response.create(200); 111 | }); 112 | } 113 | } -------------------------------------------------------------------------------- /tappablesgenerator/src/main/java/micheal65536/vienna/tappablesgenerator/ActiveTiles.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.tappablesgenerator; 2 | 3 | import com.google.gson.Gson; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import micheal65536.vienna.eventbus.client.EventBusClient; 9 | import micheal65536.vienna.eventbus.client.RequestHandler; 10 | 11 | import java.util.HashMap; 12 | import java.util.Iterator; 13 | import java.util.LinkedList; 14 | import java.util.Map; 15 | 16 | public class ActiveTiles 17 | { 18 | private static final int ACTIVE_TILE_RADIUS = 3; 19 | private static final long ACTIVE_TILE_EXPIRY_TIME = 2 * 60 * 1000; 20 | 21 | private final HashMap activeTiles = new HashMap<>(); 22 | private final ActiveTileListener activeTileListener; 23 | 24 | public ActiveTiles(@NotNull EventBusClient eventBusClient, @NotNull ActiveTileListener activeTileListener) 25 | { 26 | this.activeTileListener = activeTileListener; 27 | 28 | eventBusClient.addRequestHandler("tappables", new RequestHandler.Handler() 29 | { 30 | @Override 31 | @Nullable 32 | public String request(@NotNull RequestHandler.Request request) 33 | { 34 | if (request.type.equals("activeTile")) 35 | { 36 | record ActiveTileNotification( 37 | int x, 38 | int y, 39 | @NotNull String playerId 40 | ) 41 | { 42 | } 43 | 44 | ActiveTileNotification activeTileNotification; 45 | try 46 | { 47 | activeTileNotification = new Gson().fromJson(request.data, ActiveTileNotification.class); 48 | } 49 | catch (Exception exception) 50 | { 51 | LogManager.getLogger().error("Could not deserialise active tile notification event", exception); 52 | return null; 53 | } 54 | 55 | long currentTime = System.currentTimeMillis(); 56 | ActiveTiles.this.pruneActiveTiles(currentTime); 57 | LinkedList newActiveTiles = new LinkedList<>(); 58 | for (int tileX = activeTileNotification.x - ACTIVE_TILE_RADIUS; tileX < activeTileNotification.x + ACTIVE_TILE_RADIUS + 1; tileX++) 59 | { 60 | for (int tileY = activeTileNotification.y - ACTIVE_TILE_RADIUS; tileY < activeTileNotification.y + ACTIVE_TILE_RADIUS + 1; tileY++) 61 | { 62 | ActiveTile activeTile = ActiveTiles.this.markTileActive(tileX, tileY, currentTime); 63 | if (activeTile.latestActiveTime == activeTile.firstActiveTime) // indicating that the tile is newly-active 64 | { 65 | newActiveTiles.add(activeTile); 66 | } 67 | } 68 | } 69 | if (!newActiveTiles.isEmpty()) 70 | { 71 | ActiveTiles.this.activeTileListener.active(newActiveTiles.toArray(ActiveTile[]::new)); 72 | } 73 | 74 | return ""; 75 | } 76 | else 77 | { 78 | return null; 79 | } 80 | } 81 | 82 | @Override 83 | public void error() 84 | { 85 | LogManager.getLogger().error("Event bus subscriber error"); 86 | System.exit(1); 87 | } 88 | }); 89 | } 90 | 91 | @NotNull 92 | public ActiveTile[] getActiveTiles(long currentTime) 93 | { 94 | return this.activeTiles.values().stream().filter(activeTile -> currentTime < activeTile.latestActiveTime + ACTIVE_TILE_EXPIRY_TIME).toArray(ActiveTile[]::new); 95 | } 96 | 97 | @NotNull 98 | private ActiveTile markTileActive(int tileX, int tileY, long currentTime) 99 | { 100 | ActiveTile activeTile = this.activeTiles.getOrDefault((tileX << 16) + tileY, null); 101 | if (activeTile == null) 102 | { 103 | LogManager.getLogger().info("Tile {},{} is becoming active", tileX, tileY); 104 | activeTile = new ActiveTile(tileX, tileY, currentTime, currentTime); 105 | } 106 | else 107 | { 108 | activeTile = new ActiveTile(tileX, tileY, activeTile.firstActiveTime, currentTime); 109 | } 110 | this.activeTiles.put((tileX << 16) + tileY, activeTile); 111 | return activeTile; 112 | } 113 | 114 | private void pruneActiveTiles(long currentTime) 115 | { 116 | LinkedList inactiveTiles = new LinkedList<>(); 117 | for (Iterator> iterator = this.activeTiles.entrySet().iterator(); iterator.hasNext(); ) 118 | { 119 | Map.Entry entry = iterator.next(); 120 | ActiveTile activeTile = entry.getValue(); 121 | if (activeTile.latestActiveTime + ACTIVE_TILE_EXPIRY_TIME <= currentTime) 122 | { 123 | LogManager.getLogger().info("Tile {},{} is inactive", activeTile.tileX, activeTile.tileY); 124 | iterator.remove(); 125 | inactiveTiles.add(activeTile); 126 | } 127 | } 128 | if (!inactiveTiles.isEmpty()) 129 | { 130 | this.activeTileListener.inactive(inactiveTiles.toArray(ActiveTile[]::new)); 131 | } 132 | } 133 | 134 | public record ActiveTile( 135 | int tileX, 136 | int tileY, 137 | long firstActiveTime, 138 | long latestActiveTime 139 | ) 140 | { 141 | } 142 | 143 | public interface ActiveTileListener 144 | { 145 | void active(@NotNull ActiveTile[] activeTiles); 146 | 147 | void inactive(@NotNull ActiveTile[] activeTiles); 148 | } 149 | } -------------------------------------------------------------------------------- /eventbus/client/src/main/java/micheal65536/vienna/eventbus/client/RequestSender.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.eventbus.client; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.LinkedList; 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | 9 | public final class RequestSender 10 | { 11 | private final EventBusClient client; 12 | private final int channelId; 13 | 14 | private final ReentrantLock lock = new ReentrantLock(true); 15 | 16 | private boolean closed = false; 17 | 18 | private final LinkedList queuedRequests = new LinkedList<>(); 19 | private final LinkedList> queuedRequestResponses = new LinkedList<>(); 20 | private CompletableFuture currentPendingResponse = null; 21 | 22 | RequestSender(@NotNull EventBusClient client, int channelId) 23 | { 24 | this.client = client; 25 | this.channelId = channelId; 26 | } 27 | 28 | public void close() 29 | { 30 | this.client.removeRequestSender(this.channelId); 31 | this.client.sendMessage(this.channelId, "CLOSE"); 32 | this.closed(); 33 | } 34 | 35 | public CompletableFuture request(@NotNull String queueName, @NotNull String type, @NotNull String data) 36 | { 37 | if (!validateQueueName(queueName)) 38 | { 39 | throw new IllegalArgumentException("Queue name contains invalid characters"); 40 | } 41 | if (!validateType(type)) 42 | { 43 | throw new IllegalArgumentException("Type contains invalid characters"); 44 | } 45 | if (!validateData(data)) 46 | { 47 | throw new IllegalArgumentException("Data contains invalid characters"); 48 | } 49 | 50 | String requestMessage = "REQ " + queueName + ":" + type + ":" + data; 51 | 52 | CompletableFuture completableFuture = new CompletableFuture<>(); 53 | 54 | this.lock.lock(); 55 | if (this.closed) 56 | { 57 | completableFuture.complete(null); 58 | } 59 | else 60 | { 61 | this.queuedRequests.add(requestMessage); 62 | this.queuedRequestResponses.add(completableFuture); 63 | if (this.currentPendingResponse == null) 64 | { 65 | this.sendNextRequest(); 66 | } 67 | } 68 | this.lock.unlock(); 69 | 70 | return completableFuture; 71 | } 72 | 73 | public void flush() 74 | { 75 | this.lock.lock(); 76 | CompletableFuture completableFuture = this.queuedRequestResponses.isEmpty() ? this.currentPendingResponse : this.queuedRequestResponses.getLast(); 77 | this.lock.unlock(); 78 | 79 | if (completableFuture != null) 80 | { 81 | completableFuture.join(); 82 | } 83 | } 84 | 85 | boolean handleMessage(@NotNull String message) 86 | { 87 | if (message.equals("ERR")) 88 | { 89 | this.close(); 90 | return true; 91 | } 92 | else if (message.equals("ACK")) 93 | { 94 | return true; 95 | } 96 | else 97 | { 98 | String response; 99 | 100 | String[] parts = message.split(" ", 2); 101 | if (parts[0].equals("NREP")) 102 | { 103 | if (parts.length != 1) 104 | { 105 | return false; 106 | } 107 | response = null; 108 | } 109 | else if (parts[0].equals("REP")) 110 | { 111 | if (parts.length != 2) 112 | { 113 | return false; 114 | } 115 | response = parts[1]; 116 | } 117 | else 118 | { 119 | return false; 120 | } 121 | 122 | try 123 | { 124 | this.lock.lock(); 125 | if (this.currentPendingResponse != null) 126 | { 127 | this.currentPendingResponse.complete(response); 128 | this.currentPendingResponse = null; 129 | if (!this.queuedRequests.isEmpty()) 130 | { 131 | this.sendNextRequest(); 132 | } 133 | return true; 134 | } 135 | else 136 | { 137 | return false; 138 | } 139 | } 140 | finally 141 | { 142 | this.lock.unlock(); 143 | } 144 | } 145 | } 146 | 147 | private void sendNextRequest() 148 | { 149 | String message = this.queuedRequests.removeFirst(); 150 | this.client.sendMessage(this.channelId, message); 151 | this.currentPendingResponse = this.queuedRequestResponses.removeFirst(); 152 | } 153 | 154 | void closed() 155 | { 156 | this.lock.lock(); 157 | 158 | this.closed = true; 159 | 160 | if (this.currentPendingResponse != null) 161 | { 162 | this.currentPendingResponse.complete(null); 163 | this.currentPendingResponse = null; 164 | } 165 | this.queuedRequestResponses.forEach(completableFuture -> completableFuture.complete(null)); 166 | this.queuedRequestResponses.clear(); 167 | this.queuedRequests.clear(); 168 | 169 | this.lock.unlock(); 170 | } 171 | 172 | private static boolean validateQueueName(String queueName) 173 | { 174 | if (queueName.chars().anyMatch(character -> character < 32 || character >= 127) || queueName.isEmpty() || queueName.matches("[^A-Za-z0-9_\\-]") || queueName.matches("^[^A-Za-z0-9]")) 175 | { 176 | return false; 177 | } 178 | return true; 179 | } 180 | 181 | private static boolean validateType(String type) 182 | { 183 | if (type.chars().anyMatch(character -> character < 32 || character >= 127) || type.isEmpty() || type.matches("[^A-Za-z0-9_\\-]") || type.matches("^[^A-Za-z0-9]")) 184 | { 185 | return false; 186 | } 187 | return true; 188 | } 189 | 190 | private static boolean validateData(@NotNull String string) 191 | { 192 | return string.chars().noneMatch(character -> character < 32 || character >= 127); 193 | } 194 | } -------------------------------------------------------------------------------- /utils/cdn/src/main/java/micheal65536/vienna/utils/cdn/Main.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.utils.cdn; 2 | 3 | import jakarta.servlet.ServletException; 4 | import jakarta.servlet.http.HttpServlet; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import org.apache.catalina.Context; 8 | import org.apache.catalina.LifecycleException; 9 | import org.apache.catalina.connector.Connector; 10 | import org.apache.catalina.startup.Tomcat; 11 | import org.apache.commons.cli.CommandLine; 12 | import org.apache.commons.cli.DefaultParser; 13 | import org.apache.commons.cli.Option; 14 | import org.apache.commons.cli.Options; 15 | import org.apache.commons.cli.ParseException; 16 | import org.apache.logging.log4j.Level; 17 | import org.apache.logging.log4j.LogManager; 18 | import org.apache.logging.log4j.core.config.Configurator; 19 | import org.jetbrains.annotations.NotNull; 20 | 21 | import micheal65536.vienna.utils.http.routing.Application; 22 | import micheal65536.vienna.utils.http.routing.Request; 23 | import micheal65536.vienna.utils.http.routing.Response; 24 | import micheal65536.vienna.utils.http.routing.Router; 25 | import micheal65536.vienna.utils.http.routing.ServerErrorException; 26 | 27 | import java.io.File; 28 | import java.io.FileInputStream; 29 | import java.io.IOException; 30 | import java.nio.file.Files; 31 | import java.nio.file.attribute.BasicFileAttributes; 32 | 33 | public class Main 34 | { 35 | public static void main(String[] args) 36 | { 37 | Configurator.setRootLevel(Level.DEBUG); 38 | 39 | Options options = new Options(); 40 | options.addOption(Option.builder() 41 | .option("port") 42 | .hasArg() 43 | .argName("port") 44 | .type(Number.class) 45 | .desc("Port to listen on, defaults to 8080") 46 | .build()); 47 | options.addOption(Option.builder() 48 | .option("resourcePackPath") 49 | .hasArg() 50 | .argName("path") 51 | .desc("Resource pack path, defaults to /availableresourcepack/resourcepacks/dba38e59-091a-4826-b76a-a08d7de5a9e2-1301b0c257a311678123b9e7325d0d6c61db3c35") 52 | .build()); 53 | options.addOption(Option.builder() 54 | .option("resourcePackFile") 55 | .hasArg() 56 | .required() 57 | .argName("file") 58 | .desc("Resource pack file") 59 | .build()); 60 | CommandLine commandLine; 61 | int httpPort; 62 | String resourcePackPath; 63 | String resourcePackFile; 64 | try 65 | { 66 | commandLine = new DefaultParser().parse(options, args); 67 | httpPort = commandLine.hasOption("port") ? (int) (long) commandLine.getParsedOptionValue("port") : 8080; 68 | resourcePackPath = commandLine.hasOption("resourcePackPath") ? commandLine.getOptionValue("resourcePackPath") : "/availableresourcepack/resourcepacks/dba38e59-091a-4826-b76a-a08d7de5a9e2-1301b0c257a311678123b9e7325d0d6c61db3c35"; 69 | resourcePackFile = commandLine.getOptionValue("resourcePackFile"); 70 | } 71 | catch (ParseException exception) 72 | { 73 | LogManager.getLogger().fatal(exception); 74 | System.exit(1); 75 | return; 76 | } 77 | 78 | Application application = buildApplication(resourcePackPath, resourcePackFile); 79 | 80 | startServer(httpPort, application); 81 | } 82 | 83 | @NotNull 84 | private static Application buildApplication(@NotNull String resourcePackPath, @NotNull String resourcePackFile) 85 | { 86 | Application application = new Application(); 87 | 88 | application.router.addHandler(new Router.Route.Builder(Request.Method.HEAD, resourcePackPath).build(), request -> 89 | { 90 | try 91 | { 92 | BasicFileAttributes basicFileAttributes = Files.readAttributes(new File(resourcePackFile).toPath(), BasicFileAttributes.class); 93 | return Response.create(200).contentType("application/zip").header("Content-Length", Long.toString(basicFileAttributes.size())); 94 | } 95 | catch (IOException exception) 96 | { 97 | throw new ServerErrorException(exception); 98 | } 99 | }); 100 | application.router.addHandler(new Router.Route.Builder(Request.Method.GET, resourcePackPath).build(), request -> 101 | { 102 | byte[] data; 103 | try (FileInputStream fileInputStream = new FileInputStream(new File(resourcePackFile))) 104 | { 105 | data = fileInputStream.readAllBytes(); 106 | } 107 | catch (IOException exception) 108 | { 109 | throw new ServerErrorException(exception); 110 | } 111 | return Response.ok(data, "application/zip").header("Content-Length", Integer.toString(data.length)); 112 | }); 113 | 114 | return application; 115 | } 116 | 117 | private static void startServer(int port, @NotNull Application application) 118 | { 119 | LogManager.getLogger().info("Starting embedded Tomcat server"); 120 | File tomcatDir = null; 121 | try 122 | { 123 | tomcatDir = Files.createTempDirectory("vienna-utils-cdn-tomcat-").toFile(); 124 | } 125 | catch (IOException exception) 126 | { 127 | LogManager.getLogger().fatal("Could not start Tomcat server", exception); 128 | System.exit(1); 129 | return; 130 | } 131 | File baseDir = new File(tomcatDir, "baseDir"); 132 | baseDir.mkdir(); 133 | File docBase = new File(tomcatDir, "docBase"); 134 | docBase.mkdir(); 135 | Tomcat tomcat = new Tomcat(); 136 | tomcat.setBaseDir(baseDir.getAbsolutePath()); 137 | Connector connector = new Connector(); 138 | connector.setPort(port); 139 | tomcat.setConnector(connector); 140 | Context context = tomcat.addContext("", docBase.getAbsolutePath()); 141 | tomcat.addServlet("", "", new HttpServlet() 142 | { 143 | @Override 144 | protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException 145 | { 146 | application.handleRequest(request, response); 147 | } 148 | }); 149 | context.addServletMappingDecoded("/*", ""); 150 | try 151 | { 152 | tomcat.start(); 153 | } 154 | catch (LifecycleException exception) 155 | { 156 | LogManager.getLogger().fatal("Could not start Tomcat server", exception); 157 | System.exit(1); 158 | return; 159 | } 160 | 161 | LogManager.getLogger().info("Started"); 162 | } 163 | } -------------------------------------------------------------------------------- /utils/locator/src/main/java/micheal65536/vienna/utils/locator/Main.java: -------------------------------------------------------------------------------- 1 | package micheal65536.vienna.utils.locator; 2 | 3 | import jakarta.servlet.ServletException; 4 | import jakarta.servlet.http.HttpServlet; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import org.apache.catalina.Context; 8 | import org.apache.catalina.LifecycleException; 9 | import org.apache.catalina.connector.Connector; 10 | import org.apache.catalina.startup.Tomcat; 11 | import org.apache.commons.cli.CommandLine; 12 | import org.apache.commons.cli.DefaultParser; 13 | import org.apache.commons.cli.Option; 14 | import org.apache.commons.cli.Options; 15 | import org.apache.commons.cli.ParseException; 16 | import org.apache.logging.log4j.Level; 17 | import org.apache.logging.log4j.LogManager; 18 | import org.apache.logging.log4j.core.config.Configurator; 19 | import org.jetbrains.annotations.NotNull; 20 | 21 | import micheal65536.vienna.utils.http.routing.Application; 22 | import micheal65536.vienna.utils.http.routing.Handler; 23 | import micheal65536.vienna.utils.http.routing.Request; 24 | import micheal65536.vienna.utils.http.routing.Response; 25 | import micheal65536.vienna.utils.http.routing.Router; 26 | 27 | import java.io.File; 28 | import java.io.IOException; 29 | import java.nio.file.Files; 30 | import java.util.HashMap; 31 | 32 | public class Main 33 | { 34 | public static void main(String[] args) 35 | { 36 | Configurator.setRootLevel(Level.DEBUG); 37 | 38 | Options options = new Options(); 39 | options.addOption(Option.builder() 40 | .option("port") 41 | .hasArg() 42 | .argName("port") 43 | .type(Number.class) 44 | .desc("Port to listen on, defaults to 8080") 45 | .build()); 46 | options.addOption(Option.builder() 47 | .option("api") 48 | .hasArg() 49 | .required() 50 | .argName("address") 51 | .desc("API address") 52 | .build()); 53 | options.addOption(Option.builder() 54 | .option("cdn") 55 | .hasArg() 56 | .required() 57 | .argName("address") 58 | .desc("CDN address") 59 | .build()); 60 | options.addOption(Option.builder() 61 | .option("playfabTitleId") 62 | .hasArg() 63 | .required() 64 | .argName("id") 65 | .desc("PlayFab title ID") 66 | .build()); 67 | CommandLine commandLine; 68 | int httpPort; 69 | String apiAddress; 70 | String cdnAddress; 71 | String playfabTitleId; 72 | try 73 | { 74 | commandLine = new DefaultParser().parse(options, args); 75 | httpPort = commandLine.hasOption("port") ? (int) (long) commandLine.getParsedOptionValue("port") : 8080; 76 | apiAddress = commandLine.getOptionValue("api"); 77 | cdnAddress = commandLine.getOptionValue("cdn"); 78 | playfabTitleId = commandLine.getOptionValue("playfabTitleId"); 79 | } 80 | catch (ParseException exception) 81 | { 82 | LogManager.getLogger().fatal(exception); 83 | System.exit(1); 84 | return; 85 | } 86 | 87 | Application application = buildApplication(apiAddress, cdnAddress, playfabTitleId); 88 | 89 | startServer(httpPort, application); 90 | } 91 | 92 | @NotNull 93 | private static Application buildApplication(@NotNull String apiAddress, @NotNull String cdnAddress, @NotNull String playfabTitleId) 94 | { 95 | Application application = new Application(); 96 | 97 | Handler locatorHandler = request -> 98 | { 99 | record LocatorResponse( 100 | @NotNull Result result, 101 | @NotNull HashMap updates 102 | ) 103 | { 104 | record Result( 105 | @NotNull HashMap serviceEnvironments, 106 | @NotNull HashMap supportedEnvironments 107 | ) 108 | { 109 | record ServiceEnvironment( 110 | @NotNull String serviceUri, 111 | @NotNull String cdnUri, 112 | @NotNull String playfabTitleId 113 | ) 114 | { 115 | } 116 | } 117 | } 118 | LocatorResponse locatorResponse = new LocatorResponse(new LocatorResponse.Result(new HashMap<>(), new HashMap<>()), new HashMap<>()); 119 | locatorResponse.result.serviceEnvironments.put("production", new LocatorResponse.Result.ServiceEnvironment(apiAddress, cdnAddress, playfabTitleId)); 120 | locatorResponse.result.supportedEnvironments.put("2020.1217.02", new String[]{"production"}); 121 | return Response.okFromJson(locatorResponse, LocatorResponse.class); 122 | }; 123 | application.router.addHandler(new Router.Route.Builder(Request.Method.GET, "/player/environment").addQueryParameter("buildNumber").build(), locatorHandler); 124 | application.router.addHandler(new Router.Route.Builder(Request.Method.GET, "/api/v1.1/player/environment").addQueryParameter("buildNumber").build(), locatorHandler); // for some reason MCE sometimes includes the "/api/v1.1" prefix on the locator URL and sometimes does not 125 | 126 | return application; 127 | } 128 | 129 | private static void startServer(int port, @NotNull Application application) 130 | { 131 | LogManager.getLogger().info("Starting embedded Tomcat server"); 132 | File tomcatDir = null; 133 | try 134 | { 135 | tomcatDir = Files.createTempDirectory("vienna-utils-locator-tomcat-").toFile(); 136 | } 137 | catch (IOException exception) 138 | { 139 | LogManager.getLogger().fatal("Could not start Tomcat server", exception); 140 | System.exit(1); 141 | return; 142 | } 143 | File baseDir = new File(tomcatDir, "baseDir"); 144 | baseDir.mkdir(); 145 | File docBase = new File(tomcatDir, "docBase"); 146 | docBase.mkdir(); 147 | Tomcat tomcat = new Tomcat(); 148 | tomcat.setBaseDir(baseDir.getAbsolutePath()); 149 | Connector connector = new Connector(); 150 | connector.setPort(port); 151 | tomcat.setConnector(connector); 152 | Context context = tomcat.addContext("", docBase.getAbsolutePath()); 153 | tomcat.addServlet("", "", new HttpServlet() 154 | { 155 | @Override 156 | protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException 157 | { 158 | application.handleRequest(request, response); 159 | } 160 | }); 161 | context.addServletMappingDecoded("/*", ""); 162 | try 163 | { 164 | tomcat.start(); 165 | } 166 | catch (LifecycleException exception) 167 | { 168 | LogManager.getLogger().fatal("Could not start Tomcat server", exception); 169 | System.exit(1); 170 | return; 171 | } 172 | 173 | LogManager.getLogger().info("Started"); 174 | } 175 | } --------------------------------------------------------------------------------