diff --git a/WebHostLib/static/styles/tracker__Celeste64.css b/WebHostLib/static/styles/tracker__Celeste64.css
new file mode 100644
index 000000000000..8901adbc9f9c
--- /dev/null
+++ b/WebHostLib/static/styles/tracker__Celeste64.css
@@ -0,0 +1,143 @@
+@import url('https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@100..900&display=swap');
+
+.tracker-container {
+ width: 440px;
+ box-sizing: border-box;
+ font-family: "Lexend Deca", Arial, Helvetica, sans-serif;
+ border-radius: 4px;
+ resize: both;
+
+ color: white;
+}
+
+/** Inventory Grid ****************************************************************************************************/
+.inventory-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ padding: 1rem;
+ gap: 1rem;
+
+ margin-left: 0.5rem;
+ box-shadow: 2px 2px 4px rgba(0, 0, 0);
+}
+
+.inventory-grid .item {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ height: 48px;
+}
+
+.inventory-grid .dual-item {
+ display: flex;
+ justify-content: center;
+}
+
+.inventory-grid .missing {
+ /* Missing items will be in full grayscale to signify "uncollected". */
+ filter: grayscale(100%) contrast(75%) brightness(75%);
+}
+
+.inventory-grid .item img,
+.inventory-grid .dual-item img {
+ display: flex;
+ align-items: center;
+ text-align: center;
+ font-size: 0.8rem;
+ text-shadow: 0 1px 2px black;
+ font-weight: bold;
+ image-rendering: crisp-edges;
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+.inventory-grid .dual-item img {
+ height: 48px;
+ margin: 0 -4px;
+}
+
+.inventory-grid .dual-item img:first-child {
+ align-self: flex-end;
+}
+
+.inventory-grid .item .quantity {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ text-align: right;
+ font-weight: 600;
+ font-size: 1.75rem;
+ line-height: 1.75rem;
+ text-shadow:
+ -1px -1px 0 #000,
+ 1px -1px 0 #000,
+ -1px 1px 0 #000,
+ 1px 1px 0 #000;
+ user-select: none;
+}
+
+/** Regions List ******************************************************************************************************/
+.regions-list {
+ padding: 1rem;
+
+ margin-top: 1rem;
+ margin-left: 0.5rem;
+ box-shadow: 2px 2px 4px rgba(0, 0, 0);
+}
+
+.regions-list summary {
+ list-style: none;
+ display: flex;
+ gap: 0.5rem;
+ cursor: pointer;
+}
+
+.regions-list summary::before {
+ content: "⯈";
+ width: 1em;
+ flex-shrink: 0;
+}
+
+.regions-list details {
+ font-weight: 300;
+}
+
+.regions-list details[open] > summary::before {
+ content: "⯆";
+}
+
+.regions-list .region {
+ width: 100%;
+ display: grid;
+ grid-template-columns: 20fr 8fr 2fr 2fr;
+ align-items: center;
+ gap: 4px;
+ text-align: center;
+ font-weight: 300;
+ box-sizing: border-box;
+}
+
+.regions-list .region :first-child {
+ text-align: left;
+ font-weight: 500;
+}
+
+.regions-list .region.region-header {
+ margin-left: 24px;
+ width: calc(100% - 24px);
+ padding: 2px;
+}
+
+.regions-list .location-rows {
+ border-top: 1px solid white;
+ display: grid;
+ grid-template-columns: auto 32px;
+ font-weight: 300;
+ padding: 2px 8px;
+ margin-top: 4px;
+ font-size: 0.8rem;
+}
+
+.regions-list .location-rows :nth-child(even) {
+ text-align: right;
+}
diff --git a/WebHostLib/templates/multitracker__Celeste64.html b/WebHostLib/templates/multitracker__Celeste64.html
new file mode 100644
index 000000000000..5da7793b9036
--- /dev/null
+++ b/WebHostLib/templates/multitracker__Celeste64.html
@@ -0,0 +1,91 @@
+{% extends "multitracker.html" %}
+{% block head %}
+ {{ super() }}
+
+{% endblock %}
+
+{# Original icons located at https://github.com/PoryGone/Celeste-64-AP-Tracker/tree/main/images #}
+{# List all tracker-relevant icons. Format: (Name, Image URL) #}
+{% set icons = {
+ "Strawberry" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/strawberry.png?raw=true",
+
+ "Air Dash" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/AirDash.png?raw=true",
+ "Breakable Blocks" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Breakables_Filled.PNG?raw=true",
+ "Cassettes" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Cassettes_Filled.PNG?raw=true",
+ "Climb" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Climb.png?raw=true",
+ "Coins" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Coins_Filled.PNG?raw=true",
+ "Dash Refills" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Dash_Filled.png?raw=true",
+ "Double Dash Refills" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/DoubleDash_Filled.png?raw=true",
+ "Feathers" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Feather_Filled.png?raw=true",
+ "Ground Dash" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/GroundDash.png?raw=true",
+ "Skid Jump" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/SkidJump.png?raw=true",
+ "Springs" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Springs_Filled.png?raw=true",
+ "Traffic Blocks" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Traffic_Filled.png?raw=true",
+
+ "IsCompleted" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Baddy.png?raw=true"
+} %}
+
+{% set inventory_order = [
+ "Strawberry",
+
+ "Climb",
+ "Skid Jump",
+ "Air Dash",
+ "Ground Dash",
+
+ "Cassettes",
+ "Dash Refills",
+ "Double Dash Refills",
+ "Feathers",
+
+ "Coins",
+ "Breakable Blocks",
+ "Springs",
+ "Traffic Blocks",
+
+ "IsCompleted"
+] %}
+
+{% set multi_items = [
+ "Strawberry"
+] %}
+
+{# Most have a duplicated 0th entry for when we have none of that item to still load the correct icon/name. #}
+{% set progressive_order = {
+
+} %}
+
+{%- block custom_table_headers %}
+ {#- macro that creates a table header with display name and image -#}
+ {%- macro make_header(name, img_src) %}
+
+
+ |
+ {% endmacro -%}
+
+ {#- call the macro to build the table header -#}
+ {%- for item in inventory_order %}
+ {%- if item in icons -%}
+
+
+ |
+ {%- endif %}
+ {% endfor -%}
+{% endblock %}
+
+{# build each row of custom entries #}
+{% block custom_table_row scoped %}
+ {%- for item in inventory_order -%}
+ {%- if inventories[(team, player)][item] -%}
+
+ {% if item in multi_items %}
+ {{ inventories[(team, player)][item] }}
+ {% else %}
+ ✔️
+ {% endif %}
+ |
+ {%- else -%}
+ |
+ {%- endif -%}
+ {% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/WebHostLib/templates/tracker__Celeste64.html b/WebHostLib/templates/tracker__Celeste64.html
new file mode 100644
index 000000000000..7e819ef65503
--- /dev/null
+++ b/WebHostLib/templates/tracker__Celeste64.html
@@ -0,0 +1,170 @@
+{# Original icons located at https://github.com/PoryGone/Celeste-64-AP-Tracker/tree/main/images #}
+{% set icons = {
+ "Strawberry" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/strawberry.png?raw=true",
+
+ "Air Dash" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/AirDash.png?raw=true",
+ "Breakable Blocks" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Breakables_Filled.PNG?raw=true",
+ "Cassettes" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Cassettes_Filled.PNG?raw=true",
+ "Climb" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Climb.png?raw=true",
+ "Coins" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Coins_Filled.PNG?raw=true",
+ "Dash Refills" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Dash_Filled.png?raw=true",
+ "Double Dash Refills" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/DoubleDash_Filled.png?raw=true",
+ "Feathers" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Feather_Filled.png?raw=true",
+ "Ground Dash" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/GroundDash.png?raw=true",
+ "Skid Jump" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/SkidJump.png?raw=true",
+ "Springs" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Springs_Filled.png?raw=true",
+ "Traffic Blocks" : "https://github.com/mrkssr/ap-icons/blob/main/Celeste%2064%20Tracker/items/Traffic_Filled.png?raw=true"
+} %}
+
+{% set inventory_order = [
+ "Strawberry",
+ "Empty",
+ "Empty",
+ "Empty",
+
+ "Climb",
+ "Skid Jump",
+ "Air Dash",
+ "Ground Dash",
+
+ "Cassettes",
+ "Dash Refills",
+ "Double Dash Refills",
+ "Feathers",
+
+ "Coins",
+ "Breakable Blocks",
+ "Springs",
+ "Traffic Blocks"
+] %}
+
+{% set multi_items = [
+ "Strawberry"
+] %}
+
+{# Most have a duplicated 0th entry for when we have none of that item to still load the correct icon/name. #}
+{% set progressive_order = {
+
+} %}
+
+
+
+
+
+
+ {{ player_name }}'s Tracker
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {# Inventory Grid #}
+
+ {% for item in inventory_order %}
+ {% if item in progressive_order %}
+ {% set non_prog_item = progressive_order[item][inventory[item]] %}
+
+
+
+ {% else %}
+
+ {% if item != "Empty" %}
+
+
+ {% if item in multi_items %}
+
{{ inventory[item] }}
+ {% endif %}
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+ {% for region_name in known_regions %}
+ {% set region_data = regions[region_name] %}
+ {% if region_data["locations"] | length > 0 %}
+
+
+
+ {{ region_name }}
+ {{ region_data["checked"] }} / {{ region_data["locations"] | length }}
+
+ {% if region_data["checked"] == (region_data["locations"] | length) %}
+ ✔
+ {% else %}
+ —
+ {% endif %}
+
+
+
+
+
+ {% for location, checked in region_data["locations"] %}
+
{{ location }}
+
{% if checked %}✔{% endif %}
+ {% endfor %}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py
index 75b5fb0202d9..68cff0d1a892 100644
--- a/WebHostLib/tracker.py
+++ b/WebHostLib/tracker.py
@@ -2459,3 +2459,181 @@ def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int)
)
_player_trackers["Starcraft 2"] = render_Starcraft2_tracker
+
+if "Celeste 64" in network_data_package["games"]:
+ # Mapping from non-progressive item to progressive name and max level.
+ non_progressive_items = {
+
+ }
+
+ progressive_item_max = {
+
+ }
+
+ REGION_OVERWORLD = "Overworld"
+ REGION_CASSETTES = "Cassettes"
+ REGION_FRIENDS = "Friends"
+ REGION_SIGNS = "Signs"
+ REGION_CARS = "Cars"
+
+ known_regions = [REGION_OVERWORLD, REGION_CASSETTES, REGION_FRIENDS, REGION_SIGNS, REGION_CARS]
+
+ locations_table = {
+ 13238272: REGION_OVERWORLD,
+ 13238273: REGION_OVERWORLD,
+ 13238274: REGION_OVERWORLD,
+ 13238275: REGION_OVERWORLD,
+ 13238276: REGION_OVERWORLD,
+ 13238277: REGION_OVERWORLD,
+ 13238278: REGION_OVERWORLD,
+ 13238279: REGION_OVERWORLD,
+ 13238280: REGION_OVERWORLD,
+ 13238281: REGION_OVERWORLD,
+ 13238282: REGION_OVERWORLD,
+ 13238283: REGION_OVERWORLD,
+ 13238284: REGION_OVERWORLD,
+ 13238285: REGION_OVERWORLD,
+ 13238286: REGION_OVERWORLD,
+ 13238287: REGION_OVERWORLD,
+ 13238288: REGION_OVERWORLD,
+ 13238289: REGION_OVERWORLD,
+ 13238290: REGION_OVERWORLD,
+ 13238291: REGION_OVERWORLD,
+
+ 13238292: REGION_CASSETTES,
+ 13238293: REGION_CASSETTES,
+ 13238294: REGION_CASSETTES,
+ 13238295: REGION_CASSETTES,
+ 13238296: REGION_CASSETTES,
+ 13238297: REGION_CASSETTES,
+ 13238298: REGION_CASSETTES,
+ 13238299: REGION_CASSETTES,
+ 13238300: REGION_CASSETTES,
+ 13238301: REGION_CASSETTES,
+
+ 13238531: REGION_FRIENDS,
+ 13238532: REGION_FRIENDS,
+ 13238533: REGION_FRIENDS,
+ 13238534: REGION_FRIENDS,
+ 13238535: REGION_FRIENDS,
+ 13238536: REGION_FRIENDS,
+ 13238528: REGION_FRIENDS,
+ 13238529: REGION_FRIENDS,
+ 13238530: REGION_FRIENDS,
+
+ 13238786: REGION_SIGNS,
+ 13238787: REGION_SIGNS,
+ 13238788: REGION_SIGNS,
+ 13238784: REGION_SIGNS,
+ 13238785: REGION_SIGNS,
+
+ 13239040: REGION_CARS,
+ 13239041: REGION_CARS
+ }
+
+ def prepare_inventories(team: int, player: int, inventory: Counter[str], tracker_data: TrackerData):
+ # These settings were added in v1.2.
+ # That this tracker also work for generated seeds on earlier versions is a check if the option is even available in the slot data.
+ if not ("move_shuffle" in tracker_data.get_slot_data(team, player) and bool(tracker_data.get_slot_data(team, player)["move_shuffle"])):
+ # If moves are not shuffled add these abilities to the inventory to avoid extra handling for the collected items.
+ inventory["Climb"] = 1
+ inventory["Skid Jump"] = 1
+ inventory["Air Dash"] = 1
+ inventory["Ground Dash"] = 1
+
+ for item, (prog_item, level) in non_progressive_items.items():
+ if item in inventory:
+ inventory[prog_item] = min(max(inventory[prog_item], level), progressive_item_max[prog_item])
+
+ # Completed item if we meet goal.
+ if tracker_data.get_room_client_statuses()[team, player] == ClientStatus.CLIENT_GOAL:
+ inventory["IsCompleted"] = 1
+
+ def render_Celeste64_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]):
+ inventories: Dict[Tuple[int, int], Counter[str]] = {
+ (team, player): collections.Counter({
+ tracker_data.item_id_to_name["Celeste 64"][code]: count
+ for code, count in tracker_data.get_player_inventory_counts(team, player).items()
+ })
+ for team, players in tracker_data.get_all_players().items()
+ for player in players if tracker_data.get_slot_info(team, player).game == "Celeste 64"
+ }
+
+ # Translate non-progression items to progression items for tracker simplicity.
+ for (team, player), inventory in inventories.items():
+ prepare_inventories(team, player, inventory, tracker_data)
+
+ return render_template(
+ "multitracker__Celeste64.html",
+ enabled_trackers=enabled_trackers,
+ current_tracker="Celeste 64",
+ room=tracker_data.room,
+ all_slots=tracker_data.get_all_slots(),
+ room_players=tracker_data.get_all_players(),
+ locations=tracker_data.get_room_locations(),
+ locations_complete=tracker_data.get_room_locations_complete(),
+ total_team_locations=tracker_data.get_team_locations_total_count(),
+ total_team_locations_complete=tracker_data.get_team_locations_checked_count(),
+ player_names_with_alias=tracker_data.get_room_long_player_names(),
+ completed_worlds=tracker_data.get_team_completed_worlds_count(),
+ games=tracker_data.get_room_games(),
+ states=tracker_data.get_room_client_statuses(),
+ hints=tracker_data.get_team_hints(),
+ activity_timers=tracker_data.get_room_last_activity(),
+ videos=tracker_data.get_room_videos(),
+ item_id_to_name=tracker_data.item_id_to_name,
+ location_id_to_name=tracker_data.location_id_to_name,
+ inventories=inventories
+ )
+
+ def render_Celeste64_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
+ inventory = collections.Counter({
+ tracker_data.item_id_to_name["Celeste 64"][code]: count
+ for code, count in tracker_data.get_player_inventory_counts(team, player).items()
+ })
+
+ # Translate non-progression items to progression items for tracker simplicity.
+ prepare_inventories(team, player, inventory, tracker_data)
+
+ # These settings were added in v1.2.
+ # That this tracker also work for generated seeds on earlier versions is a check if the option is even available in the slot data.
+ friendSanity = "friendsanity" in tracker_data.get_slot_data(team, player) and bool(tracker_data.get_slot_data(team, player)["friendsanity"])
+ signSanity = "signsanity" in tracker_data.get_slot_data(team, player) and bool(tracker_data.get_slot_data(team, player)["signsanity"])
+ carSanity = "carsanity" in tracker_data.get_slot_data(team, player) and bool(tracker_data.get_slot_data(team, player)["carsanity"])
+ knowRegionsCopy = known_regions.copy() # lifecycle thing
+
+ if not friendSanity : knowRegionsCopy.remove(REGION_FRIENDS)
+ if not signSanity : knowRegionsCopy.remove(REGION_SIGNS)
+ if not carSanity : knowRegionsCopy.remove(REGION_CARS)
+
+ regions = {
+ region_name: {
+ "checked": sum(
+ 1 for id, region in locations_table.items()
+ if region == region_name and id in tracker_data.get_player_checked_locations(team, player)
+ ),
+ "locations": [
+ (
+ tracker_data.location_id_to_name["Celeste 64"][id],
+ id in tracker_data.get_player_checked_locations(team, player)
+ )
+ for id, region in locations_table.items()
+ if region == region_name
+ ],
+ }
+ for region_name in knowRegionsCopy
+ }
+
+ return render_template(
+ template_name_or_list="tracker__Celeste64.html",
+ room=tracker_data.room,
+ team=team,
+ player=player,
+ inventory=inventory,
+ player_name=tracker_data.get_player_name(team, player),
+ regions=regions,
+ known_regions=knowRegionsCopy
+ )
+
+ _multiworld_trackers["Celeste 64"] = render_Celeste64_multiworld_tracker
+ _player_trackers["Celeste 64"] = render_Celeste64_tracker
\ No newline at end of file