Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2c7826e
Option to sort things by ID instead of Name
silasary Dec 10, 2024
f139adc
Merge branch 'sort-by-id' of https://github.com/ManualForArchipelago/…
nicopop Feb 19, 2025
ce5efa9
Add setting and rename Meta sorting preference
nicopop Feb 19, 2025
083ba63
update client sorting to use settings if available
nicopop Feb 19, 2025
16e9610
wip command to change sorting
nicopop Feb 19, 2025
1ab75ac
Make the settings work
nicopop Feb 20, 2025
ee987b3
make switching sorting order work
nicopop Feb 20, 2025
5df0045
fix command when not auth
nicopop Feb 20, 2025
692bc57
small fix
nicopop Feb 20, 2025
ff180ed
set default sorting order to id
nicopop Feb 20, 2025
5cb4b86
Remove sort-by-id entirely, add custom sort-key that devs can use
silasary Feb 25, 2025
3309597
Merge branch 'main' into sort-key
silasary Feb 25, 2025
c5154b7
Clean up default settings
silasary Feb 25, 2025
bd79658
Completely remove references to recommended
silasary Feb 25, 2025
68eb849
add Natural Sorting
nicopop Apr 21, 2025
bc3d2c3
Merge branch 'main' into dev_recommended_sorting
nicopop Apr 29, 2025
a1cdc5f
Merge branch 'main' into dev_recommended_sorting
nicopop Sep 3, 2025
b31d033
Merge branch 'main' of https://github.com/ManualForArchipelago/Manual…
nicopop Sep 18, 2025
658e45c
regrouped default sorting options to be in the client and correct inv…
nicopop Sep 18, 2025
cdb3fc4
made both sorting commands return a list of valid values with descrip…
nicopop Sep 18, 2025
025a57b
small schema tweak
nicopop Sep 18, 2025
450a9e7
bump client version + small tweak to make type checker happy
nicopop Sep 29, 2025
1d1177e
Merge branch 'main' into dev_recommended_sorting
nicopop Oct 13, 2025
89dd5df
fix missing comma in merged items.json
nicopop Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions schemas/Manual.items.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@
"description": "(Optional) Skips the item ID forward to the given value.\nThis can be used to provide buffer space for future items.",
"type": "integer"
},
"sort-key": {
"description": "(Optional) A string to sort the items by. If not provided, items will always be sorted by name.",
"type": "string"
},
"_comment": {"$ref": "#/definitions/comment"}
},
"required": ["name"]
Expand Down
4 changes: 4 additions & 0 deletions schemas/Manual.locations.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@
"description": "(Optional) Skips the item ID forward to the given value.\nThis can be used to provide buffer space for future items.",
"type": "integer"
},
"sort-key": {
"description": "(Optional) A string to sort the locations by. If not provided, locations will always be sorted by name.",
"type": "string"
},
"_comment": {"$ref": "#/definitions/comment"}
},
"required": ["name"]
Expand Down
169 changes: 161 additions & 8 deletions src/ManualClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import sys
import time
import typing
from typing import Any, Optional
from typing import Any, Dict, List, Optional
from enum import IntEnum

import requests
from worlds import AutoWorldRegister, network_data_package
from worlds.LauncherComponents import icon_paths
import json
import traceback


import ModuleUpdate
ModuleUpdate.update()

Expand All @@ -34,6 +34,36 @@
if typing.TYPE_CHECKING:
import kvui

class SortingOrderLoc(IntEnum):
custom = 1
inverted_custom = -1
alphabetical = 2
inverted_alphabetical = -2
natural = 3
inverted_natural = -3
default = 3

# Docs must be done after because otherwise __doc__ return none
SortingOrderLoc.custom.__doc__ = "Sort alphabetically using the custom sorting keys defined in locations.json if present, and the name otherwise."
SortingOrderLoc.alphabetical.__doc__ = "Sort alphabetically using the name of item defined in locations.json."
SortingOrderLoc.natural.__doc__ = "Sort like custom but makes sure that any number are read as integer and thus sorted naturally. EG. key2 < key12"

class SortingOrderItem(IntEnum):
custom = 1
inverted_custom = -1
alphabetical = 2
inverted_alphabetical = -2
natural = 3
inverted_natural = -3
received = 4
inverted_received = -4
default = 4

SortingOrderItem.custom.__doc__ = "Sort alphabetically using the custom sorting keys defined in items.json if present, and the name otherwise."
SortingOrderItem.alphabetical.__doc__ = "Sort alphabetically using the name of item defined in items.json."
SortingOrderItem.natural.__doc__ = "Sort like custom but makes sure that any number are read as integer and thus sorted naturally. EG. key2 < key12"
SortingOrderItem.received.__doc__ = "Sort the item in the order they are received from the server"

class ManualClientCommandProcessor(ClientCommandProcessor):
def _cmd_resync(self) -> bool:
"""Manually trigger a resync."""
Expand All @@ -53,6 +83,63 @@ def _cmd_send(self, location_name: str) -> bool:
location_id = self.ctx.location_names_to_id[location_name]
self.ctx.locations_checked.append(location_id)
self.ctx.syncing = True
return True
else:
self.output(response)
return False

@mark_raw
def _cmd_items_sorting(self, algorithm: Optional[str] = None) -> bool:
"""Set or get the current items sorting algorithm."""
return self.sorting_commands_logic(algorithm, target_items=True)

@mark_raw
def _cmd_locations_sorting(self, algorithm: Optional[str] = None) -> bool:
"""Set or get the current locations sorting algorithm."""
return self.sorting_commands_logic(algorithm, target_items=False)

def sorting_commands_logic(self, algorithm: Optional[str] = None, target_items: bool = False) -> bool:
valid_algorithms: type[SortingOrderLoc] | type[SortingOrderItem] = SortingOrderLoc
if target_items:
valid_algorithms = SortingOrderItem

valid_algorithms_names = [e.name for e in valid_algorithms] + ["default"]

if algorithm is None: #Get
cur_sort = self.ctx.items_sorting if target_items else self.ctx.locations_sorting #type: ignore
output = f"Currently {'Items' if target_items else 'Locations'} are sorted by: {cur_sort} \
\nValid sorting algorithms are:"
for algo in valid_algorithms_names:
if algo.startswith("inverted_") or algo == "default":
continue
else:
output += f"\n - {algo}/inverted_{algo}: {valid_algorithms[algo].__doc__}"
output += f"\n - default: Set sorting back to default aka '{valid_algorithms.default.name}'"

self.output(output)

return True

# Set
algorithm, usable, response = Utils.get_intended_text(
algorithm,
valid_algorithms_names
)

if usable:
if algorithm == "default":
algorithm = valid_algorithms.default.name

if target_items:
self.ctx.items_sorting = algorithm
else:
self.ctx.locations_sorting = algorithm
self.ctx.ui.build_tracker_and_locations_table() #The best place I could find to sort the locations

self.ctx.ui.request_update_tracker_and_locations_table()
self.ctx.save_options()
self.output(f"Set {'Items' if target_items else 'Locations'} sorting algorithm to {algorithm}")
return True
else:
self.output(response)
return False
Expand Down Expand Up @@ -80,6 +167,9 @@ class ManualContext(SuperContext):
deathlink_out = False

search_term = ""
settings = None
items_sorting = SortingOrderItem.default.name
locations_sorting = SortingOrderLoc.default.name

colors = {
'location_default': [219/255, 218/255, 213/255, 1],
Expand Down Expand Up @@ -131,6 +221,24 @@ async def server_auth(self, password_requested: bool = False):
self.victory_names = ["__Manual Game Complete__"]
self.goal_location = self.get_location_by_name("__Manual Game Complete__")

self.settings = Utils.get_settings().get("manual_settings", None) #.get(self.game.lower(), None)
if self.settings is not None:
has_error = False
if hasattr(self.settings, "items_sorting_order"):
self.items_sorting = self.settings.items_sorting_order
if self.items_sorting not in SortingOrderItem._member_names_:
has_error = True
self.items_sorting = SortingOrderItem.default.name

if hasattr(self.settings, "locations_sorting_order"):
self.locations_sorting = self.settings.locations_sorting_order
if self.locations_sorting not in SortingOrderLoc._member_names_:
has_error = True
self.locations_sorting = SortingOrderLoc.default.name

if has_error:
self.save_options()

await self.get_username()
await self.send_connect()

Expand Down Expand Up @@ -181,6 +289,12 @@ def set_search(self, search_term: str):
def clear_search(self):
self.search_term = ""

def save_options(self):
if self.settings is not None:
self.settings.items_sorting_order = self.items_sorting
self.settings.locations_sorting_order = self.locations_sorting
Utils.get_settings().save()

@property
def endpoints(self):
if self.server:
Expand Down Expand Up @@ -375,7 +489,7 @@ def build(self) -> Layout:
def clear_lists(self):
self.listed_items = {"(No Category)": []}
self.item_categories = ["(No Category)"]
self.listed_locations = {"(No Category)": [], "(Hinted)": []}
self.listed_locations: Dict[str, List[int]] = {"(No Category)": [], "(Hinted)": []}
self.location_categories = ["(No Category)", "(Hinted)"]

def set_active_item_accordion(self, instance):
Expand Down Expand Up @@ -521,8 +635,26 @@ def build_tracker_and_locations_table(self):
if not victory_categories:
victory_categories.add("(No Category)")

for category in self.listed_locations:
self.listed_locations[category].sort()
loc_sorting = SortingOrderLoc[self.ctx.locations_sorting]

if abs(loc_sorting) == SortingOrderLoc.alphabetical:
for category in self.listed_locations:
self.listed_locations[category].sort(key=self.ctx.location_names.lookup_in_game, reverse=loc_sorting < 0)
elif abs(loc_sorting) == SortingOrderLoc.custom:
for category in self.listed_locations:
self.listed_locations[category].sort(key=lambda i: self.ctx.get_location_by_id(i).get("sort-key", self.ctx.get_location_by_id(i).get("name", "")), \
reverse=loc_sorting < 0)

elif abs(loc_sorting) == SortingOrderLoc.natural:
# Modified from https://stackoverflow.com/a/11150413
convert = lambda text: int(text) if text.isdigit() else text.lower()
alphanum_key = lambda i: [
convert(c) for c in re.split('([0-9]+)', \
self.ctx.get_location_by_id(i).get("sort-key", self.ctx.get_location_by_id(i).get("name", "")))
]
for category in self.listed_locations:
self.listed_locations[category].sort(key=alphanum_key, reverse=loc_sorting < 0)


items_length = len(self.ctx.items_received)
tracker_panel_scrollable = TrackerLayoutScrollable(do_scroll=(False, True), bar_width=10)
Expand Down Expand Up @@ -683,9 +815,30 @@ def update_tracker_and_locations_table(self, update_highlights=False):
category_unique_name_count = 0

# Label (for all item listings)
sorted_items_received = sorted([
i.item for i in self.ctx.items_received
])
item_sorting = SortingOrderItem[self.ctx.items_sorting]
sorted_items_received = [i.item for i in self.ctx.items_received]

if abs(item_sorting) == SortingOrderItem.alphabetical:
sorted_items_received = sorted(sorted_items_received,
key=self.ctx.item_names.lookup_in_game,
reverse=item_sorting < 0)
elif abs(item_sorting) == SortingOrderItem.custom:
sorted_items_received = sorted(sorted_items_received,
key=lambda i: self.ctx.get_item_by_id(i).get("sort-key", self.ctx.get_item_by_id(i).get("name", "")),
reverse=item_sorting < 0)

elif abs(item_sorting) == SortingOrderItem.natural:
convert = lambda text: int(text) if text.isdigit() else text.lower()
alphanum_key = lambda i: [
convert(c) for c in re.split('([0-9]+)', \
self.ctx.get_item_by_id(i).get("sort-key", self.ctx.get_item_by_id(i).get("name", "")))
]
sorted_items_received = sorted(sorted_items_received,
key=alphanum_key, reverse=item_sorting < 0)

elif abs(item_sorting) == SortingOrderItem.received:
if item_sorting < 0:
sorted_items_received.reverse()

for network_item in sorted_items_received:
item_name = self.ctx.item_names.lookup_in_game(network_item)
Expand Down
1 change: 1 addition & 0 deletions src/Meta.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

from BaseClasses import Tutorial
from enum import Enum
from worlds.AutoWorld import World, WebWorld
from .Data import meta_table
from .Helpers import convert_to_long_string
Expand Down
20 changes: 18 additions & 2 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import logging
import os
import json
from typing import Callable, Optional, Counter, Any
from typing import Callable, Optional, Union, ClassVar, Counter, Any
import webbrowser
import settings

import Utils
from worlds.generic.Rules import forbid_items_for_player
Expand Down Expand Up @@ -37,10 +38,25 @@
after_collect_item, after_remove_item
from .hooks.Data import hook_interpret_slot_data

class ManualSettings(settings.Group):
class ItemsSorting(str):
"""Set your preferred Items sorting order
You can get the valid options by doing /items_sorting in the client
"""
class LocationsSorting(str):
"""Set your preferred Locations sorting order
You can get the valid options by doing /locations_sorting in the client
"""

items_sorting_order: ItemsSorting = ItemsSorting("default")
locations_sorting_order: LocationsSorting = LocationsSorting("default")

class ManualWorld(World):
__doc__ = world_description
game: str = game_name
web = world_webworld
settings: ClassVar[ManualSettings]
settings_key: ClassVar[str] = "manual_settings"

options_dataclass = manual_options_data
data_version = 2
Expand Down Expand Up @@ -548,7 +564,7 @@ def __init__(self, display_name: str, script_name: Optional[str] = None, func: O
self.version = version

def add_client_to_launcher() -> None:
version = 2025_08_12 # YYYYMMDD
version = 2025_09_29 # YYYYMMDD
found = False

if "manual" not in icon_paths:
Expand Down
Loading
Loading