aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2025-07-31 02:33:01 +0200
committerLinnea Gräf <nea@nea.moe>2025-07-31 16:45:15 +0200
commit8d655a4008d8fba0ca6f477be0e051924dc67ec6 (patch)
tree6d0d45db45822448c23d4499ba01d1633cc8bbb3 /src
parentf66cd746b1d427339ac7069b82e5aa93444a852c (diff)
downloadSkyblocker-8d655a4008d8fba0ca6f477be0e051924dc67ec6.tar.gz
Skyblocker-8d655a4008d8fba0ca6f477be0e051924dc67ec6.tar.bz2
Skyblocker-8d655a4008d8fba0ca6f477be0e051924dc67ec6.zip
Add some basic models for the profile viewer
Diffstat (limited to 'src')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ApiProfile.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ApiProfileResponse.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/CommunityUpgrades.java38
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/CoopBanking.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Currencies.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/DefaultCatacombs.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Dungeons.java58
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/FairySouls.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java133
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ProfileMember.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ProfileMemberProfile.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Slayer.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/SlayerBoss.java32
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Treasures.java80
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/package-info.java11
15 files changed, 504 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ApiProfile.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ApiProfile.java
new file mode 100644
index 00000000..4864ca52
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ApiProfile.java
@@ -0,0 +1,17 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Map;
+import java.util.UUID;
+
+public class ApiProfile {
+ @SerializedName("profile_id")
+ public UUID profileId = UUID.randomUUID();
+ public CommunityUpgrades communityUpgrades = new CommunityUpgrades();
+ public Map<UUID, ProfileMember> members = Map.of();
+ public CoopBanking banking = new CoopBanking();
+ @SerializedName("cute_name")
+ public String cuteName;
+ public boolean selected = false;
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ApiProfileResponse.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ApiProfileResponse.java
new file mode 100644
index 00000000..18adf3ef
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ApiProfileResponse.java
@@ -0,0 +1,10 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import java.util.List;
+
+/**
+ * Object mapping for the success response of {@code /v2/skyblock/profiles}.
+ */
+public class ApiProfileResponse {
+ public List<ApiProfile> profiles = List.of();
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/CommunityUpgrades.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/CommunityUpgrades.java
new file mode 100644
index 00000000..8fe83902
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/CommunityUpgrades.java
@@ -0,0 +1,38 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+public class CommunityUpgrades {
+// @Nullable
+// @SerializedName("currently_upgrading")
+// public String currentlyUpgrading = null;
+ @SerializedName("upgrade_states")
+ public List<UpgradeState> upgradeStates = List.of();
+
+ public int getUpgradeTier(String upgradeId) {
+ int tier = 0;
+ for (var upgradeState : upgradeStates) {
+ if (!Objects.equals(upgradeId, upgradeState.upgradeId))
+ continue;
+ tier = Math.max(tier, upgradeState.tier);
+ }
+ return tier;
+ }
+
+ public static class UpgradeState {
+ @SerializedName("upgrade")
+ public String upgradeId;
+ public int tier;
+ @SerializedName("started_ms")
+ public long startedMs;
+ @SerializedName("started_by")
+ public UUID startedBy;
+ @SerializedName("claimed_by")
+ public UUID claimedBy;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/CoopBanking.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/CoopBanking.java
new file mode 100644
index 00000000..2d331099
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/CoopBanking.java
@@ -0,0 +1,28 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class CoopBanking {
+ /**
+ * @see ProfileMemberProfile#personalBankAccount
+ */
+ public double balance;
+ public List<Transaction> transactions = List.of();
+
+ public static class Transaction {
+ public double amount;
+ public long timestamp;
+ public BankAction action;
+ @SerializedName("initiator_name")
+ public String initiatorName;
+ }
+
+ public enum BankAction {
+ DEPOSIT,
+ WITHDRAW,
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Currencies.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Currencies.java
new file mode 100644
index 00000000..f682ff4e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Currencies.java
@@ -0,0 +1,17 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Map;
+
+public class Currencies {
+ @SerializedName("coin_purse")
+ public double coinsInPurse;
+ @SerializedName("motes_purse")
+ public double riftMotes;
+ public Map<String, EssenceCurrency> essence = Map.of();
+
+ public static class EssenceCurrency {
+ public int current;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/DefaultCatacombs.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/DefaultCatacombs.java
new file mode 100644
index 00000000..663b2947
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/DefaultCatacombs.java
@@ -0,0 +1,21 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The default catacombs info contains some extra stats that are shared across master mode and normal mode.
+ * I am however not entirely sure everything in here is fully shared between the two modes.
+ */
+public class DefaultCatacombs extends GenericCatacombs {
+ public double experience;
+ /**
+ * Aggregate attempts (including failures) across master mode and regular mode.
+ */
+ @SerializedName("times_played")
+ public AggregateStat timesPlayed;
+
+ @SerializedName("watcher_kills")
+ public AggregateStat watcherKills;
+
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Dungeons.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Dungeons.java
new file mode 100644
index 00000000..5b6c16d8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Dungeons.java
@@ -0,0 +1,58 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.util.Map;
+
+public class Dungeons {
+ @SerializedName("last_dungeon_run")
+ public String lastDungeonRun;
+ public int secrets;
+ @SerializedName("selected_dungeon_class")
+ public String selectedDungeonClass;
+ @SerializedName("daily_runs")
+ public DailyRuns dailyRuns = new DailyRuns();
+ /**
+ * Croesus storage data
+ */
+ public Treasures treasures = new Treasures();
+ @SerializedName("player_classes")
+ public Map<String, ClassStats> classStats = Map.of();
+ @SerializedName("dungeon_types")
+ public PerDungeonType dungeonInfo;
+
+ public static class PerDungeonType {
+ @SerializedName("master_catacombs")
+ public GenericCatacombs masterModeCatacombs = new GenericCatacombs();
+ public DefaultCatacombs catacombs = new DefaultCatacombs();
+ }
+
+ public static class ClassStats {
+ public double experience;
+
+ public LevelFinder.LevelInfo getLevelInfo() {
+ return LevelFinder.getLevelInfo("Catacombs", (long) experience);
+ }
+ }
+
+ public static class DailyRuns {
+ /**
+ * This is days since UNIX epoch.
+ */
+ @SerializedName("current_day_stamp")
+ public int currentDayStamp;
+
+ public @Nullable LocalDate getLastDailyRunDate() {
+ if (currentDayStamp == 0)
+ return null;
+ return LocalDate.ofEpochDay(currentDayStamp);
+ }
+
+ @SerializedName("completed_runs_count")
+ public int completedRunsCount;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/FairySouls.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/FairySouls.java
new file mode 100644
index 00000000..5efb97e8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/FairySouls.java
@@ -0,0 +1,12 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+public class FairySouls {
+ @SerializedName("fairy_exchanges")
+ public int fairyExchanges;
+ @SerializedName("total_collected")
+ public int totalCollected;
+ @SerializedName("unspent_souls")
+ public int unspentSouls;
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java
new file mode 100644
index 00000000..90e625de
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java
@@ -0,0 +1,133 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+import net.fabricmc.loader.api.metadata.Person;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+public class GenericCatacombs {
+ @SerializedName("best_score")
+ public PersonalBest bestScore = new PersonalBest();
+ @SerializedName("fastest_time")
+ public PersonalBest fastestTime = new PersonalBest();
+ @SerializedName("mobs_killed")
+ public AggregateStat mobsKilled = new AggregateStat();
+ @SerializedName("most_mobs_killed")
+ public PersonalBest mobsKilledInOneRun = new PersonalBest();
+ @SerializedName("most_damage_mage")
+ public PersonalBest mostMageDamage = new PersonalBest();
+ /**
+ * Is this only for healer role or in general?
+ */
+ @SerializedName("most_healing")
+ public PersonalBest mostHealing = new PersonalBest();
+
+ @SerializedName("tier_completions")
+ public AggregateStat tierCompletions = new AggregateStat();
+ /**
+ * Is this adding the milestones achieved across runs? Definitely a weird metric to track, and then not include any recent update in the API.
+ */
+ @SerializedName("milestone_completions")
+ public AggregateStat milestoneCompletions = new AggregateStat();
+
+ @SerializedName("most_damage_tank")
+ public PersonalBest mostDamageTank = new PersonalBest();
+ @SerializedName("fastest_time_s")
+ public PersonalBest fastestTimeS = new PersonalBest();
+ @SerializedName("fastest_time_s_plus")
+ public PersonalBest fastestTimeSPlus = new PersonalBest();
+ @SerializedName("most_damage_archer")
+ public PersonalBest mostDamageArcher = new PersonalBest();
+ @SerializedName("most_damage_berserk")
+ public PersonalBest mostDamageBeserk = new PersonalBest();
+ @SerializedName("most_damage_healer")
+ public PersonalBest mostDamageHealer = new PersonalBest();
+
+ @SerializedName("highest_tier_completed")
+ public int highestTierCompleted;
+
+ /**
+ * Mapping of 0 indexed floor to a list of "best runs". This might be the run in which one of the {@link PersonalBest personal bests} was achieved, but i am not entirely sure.
+ */
+ @SerializedName("best_runs")
+ public Map<String, List<BestRun>> bestRuns = Map.of();
+
+ public static class BestRun {
+ public long timestamp;
+ @SerializedName("score_exploration")
+ public int scoreExploration;
+ @SerializedName("score_speed")
+ public int scoreSpeed;
+ @SerializedName("score_skill")
+ public int scoreSkill;
+ @SerializedName("score_bonus")
+ public int scoreBonus;
+ /**
+ * One of {@code tank}, {@code healer}, etc.
+ */
+ @SerializedName("dungeon_class")
+ public String dungeonClass;
+ public List<UUID> teammates = List.of();
+ @SerializedName("elapsed_time")
+ public int elapsedTime;
+ @SerializedName("damage_dealt")
+ public double damageDealt;
+ public int deaths;
+ @SerializedName("mobs_killed")
+ public int mobsKilled;
+ @SerializedName("secrets_found")
+ public int secretsFound;
+ @SerializedName("damage_mitigated")
+ public double damageMitigated;
+ @SerializedName("ally_healing")
+ public double allyHealing;
+ }
+
+ public static class PerFloorDisambiguation {
+ @SerializedName("0")
+ public @Nullable Double entrance;
+ @SerializedName("1")
+ public @Nullable Double one;
+ @SerializedName("2")
+ public @Nullable Double two;
+ @SerializedName("3")
+ public @Nullable Double three;
+ @SerializedName("4")
+ public @Nullable Double four;
+ @SerializedName("5")
+ public @Nullable Double five;
+ @SerializedName("6")
+ public @Nullable Double six;
+ @SerializedName("7")
+ public @Nullable Double seven;
+
+ /**
+ * @param oneIndexedFloor one indexed floor (F1 = 1), with Entrance = 0.
+ */
+ public @Nullable Double getBest(int oneIndexedFloor) {
+ return switch (oneIndexedFloor) {
+ case 0 -> entrance;
+ case 1 -> one;
+ case 2 -> two;
+ case 3 -> three;
+ case 4 -> four;
+ case 5 -> five;
+ case 6 -> six;
+ case 7 -> seven;
+
+ default -> throw new IllegalStateException("Unexpected floor: " + oneIndexedFloor);
+ };
+ }
+ }
+
+ public static class PersonalBest extends PerFloorDisambiguation {
+ public @Nullable Double best;
+ }
+
+ public static class AggregateStat extends PerFloorDisambiguation {
+ public @Nullable Double total;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ProfileMember.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ProfileMember.java
new file mode 100644
index 00000000..8eb0b41c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ProfileMember.java
@@ -0,0 +1,21 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Map;
+import java.util.UUID;
+
+public class ProfileMember {
+ @SerializedName("player_id")
+ public UUID playerId;
+ /**
+ * Nota bene: this is for item collections, for boss collections you need to manually add up the boss kill counts.
+ */
+ public Map<String, Integer> collection = Map.of();
+ public Slayer slayer = new Slayer();
+ @SerializedName("fairy_soul")
+ public FairySouls fairySouls = new FairySouls();
+ public ProfileMemberProfile profile = new ProfileMemberProfile();
+ public Currencies currencies = new Currencies();
+ public Dungeons dungeons = new Dungeons();
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ProfileMemberProfile.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ProfileMemberProfile.java
new file mode 100644
index 00000000..51c0cc8b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/ProfileMemberProfile.java
@@ -0,0 +1,16 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+public class ProfileMemberProfile {
+ @SerializedName("first_join")
+ public long firstJoin;
+ public int personal_bank_upgrade;
+ public boolean cookie_buff_active;
+ // TODO: deletion_notice, coop_invitation
+ /**
+ * @see ApiProfile#banking
+ */
+ @SerializedName("bank_account")
+ public double personalBankAccount;
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Slayer.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Slayer.java
new file mode 100644
index 00000000..0adb9054
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Slayer.java
@@ -0,0 +1,10 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Map;
+
+public class Slayer {
+ @SerializedName("slayer_bosses")
+ public Map<String, SlayerBoss> slayerBosses = Map.of();
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/SlayerBoss.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/SlayerBoss.java
new file mode 100644
index 00000000..215b6b0e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/SlayerBoss.java
@@ -0,0 +1,32 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Map;
+
+public class SlayerBoss {
+ @SerializedName("claimed_levels")
+ public Map<String, Boolean> claimedLevels = Map.of();
+ @SerializedName("boss_kills_tier_0")
+ int bossKills0;
+ @SerializedName("boss_kills_tier_1")
+ int bossKills1;
+ @SerializedName("boss_kills_tier_2")
+ int bossKills2;
+ @SerializedName("boss_kills_tier_3")
+ int bossKills3;
+ @SerializedName("boss_kills_tier_4")
+ int bossKills4;
+ public int xp;
+
+ public int getBossKillsByZeroIndexedTier(int tier) {
+ return switch (tier) {
+ case 0 -> bossKills0;
+ case 1 -> bossKills1;
+ case 2 -> bossKills2;
+ case 3 -> bossKills3;
+ case 4 -> bossKills4;
+ default -> 0;
+ };
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Treasures.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Treasures.java
new file mode 100644
index 00000000..47eb433f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/Treasures.java
@@ -0,0 +1,80 @@
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+import java.util.UUID;
+
+public class Treasures {
+ public List<Run> runs = List.of();
+ /**
+ * This is a list of chests. Nota bene: the chests are not grouped by dungeon run, but have a run id that can be used to group them.
+ */
+ public List<Chest> chests = List.of();
+ // TODO: add a method to collate runs and chests
+
+ public static class Chest {
+ @SerializedName("run_id")
+ public UUID runId;
+ @SerializedName("chest_id")
+ public UUID chestId;
+
+ /**
+ * The chest type, one of {@code wood}, {@code gold}, {@code diamond}, {@code emerald}, {@code obsidian}, {@code bedrock}.
+ */
+ @SerializedName("treasure_type")
+ public String treasureType;
+
+ public Rewards rewards = new Rewards();
+
+ public int quality;
+ @SerializedName("shiny_eligible")
+ public boolean shinyEligible;
+ public boolean paid;
+ public int rerolls;
+ }
+
+ public static class Rewards {
+ /**
+ * List of rewards found in a chest. These are not item ids directly:
+ * <ul>
+ * <li>Enchanted books in the form of {@code combo_1}, {@code feather_falling_6}</li>
+ * <li>Enchanted ultimate books in the form of {@code wise_1} (without {@code ultimate})</li>
+ * <li>Essence in the form of {@code ESSENCE:UNDEAD:22}, {@code ESSENCE:WITHER:30}</li>
+ * <li>Items also have a completely arbitrary form sometimes: {@code master_jerry} {@code dark_orb_1} (with stack size) {@code shadow_boots} (unstackable without stack size, but with differing id from the nbt id)</li>
+ * <li>Realistically the only way to maintain a mapping for this is a repo file.</li>
+ * </ul>
+ */
+ public List<String> rewards = List.of();
+
+ @SerializedName("rolled_rng_meter_randomly")
+ public boolean rolledRngMeterRandomly;
+ }
+
+ public static class Run {
+ @SerializedName("run_id")
+ public UUID runId;
+ @SerializedName("completion_ts")
+ public long completionTimestamp;
+ /**
+ * {@code "master_catacombs"} or {@link "catacombs"} TODO: CITATION REQUIRED
+ */
+ @SerializedName("dungeon_type")
+ public String dungeonType;
+ @SerializedName("dungeon_tier")
+ public int dungeonTier;
+ public List<Participant> participants = List.of();
+ }
+
+ public static class Participant {
+ @SerializedName("player_uuid")
+ public UUID playerUUID;
+ /**
+ * This is formatted as {@code [name]: [role] ([class level])} + some colour codes (probably depending on level). Notably the class level is not a roman numeral, but instead an arabic one.
+ */
+ @SerializedName("display_name")
+ public String display_name;
+ @SerializedName("class_milestone")
+ public int classMilestone;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/package-info.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/package-info.java
new file mode 100644
index 00000000..de5cc978
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/package-info.java
@@ -0,0 +1,11 @@
+/**
+ * Package containing models for the JSON responses used by the profile viewer.
+ * {@code class}es are intentionally used instead of records, since specifying defaults for records is more cumbersome.
+ * This package is {@link org.jetbrains.annotations.NotNullByDefault not null by default}, meaning warnings are emitted
+ * if a field is not explicitly marked as nullable or has a default value provided. This should be a sufficient safeguard
+ * against most API deviations (such as missing fields).
+ */
+@NotNullByDefault
+package de.hysky.skyblocker.skyblock.profileviewer.model;
+
+import org.jetbrains.annotations.NotNullByDefault;