From bec632fab9975e3641e8981ff727511202f00a24 Mon Sep 17 00:00:00 2001 From: Bryce Mecum Date: Sat, 24 Oct 2026 10:45:34 -0700 Subject: [PATCH] Re-order servers --- Servers.xml | 342 ++++++++++++++++---------------- scripts/reorder_servers.py | 194 ++++++++++++++++++ scripts/test_reorder_servers.py | 155 +++++++++++++++ 3 files changed, 520 insertions(+), 171 deletions(-) create mode 100644 scripts/reorder_servers.py create mode 100644 scripts/test_reorder_servers.py diff --git a/Servers.xml b/Servers.xml index e3fce0e..1e28b21 100644 --- a/Servers.xml +++ b/Servers.xml @@ -1,101 +1,5 @@ - - 7f3b9c21-4e8a-4d16-b2f7-1a9e5c0d3b84 - Verland_Hrim - PvE End of Retail. Quality of Life + Improved Society Rewards. 3 account limit. All Welcome. - ACE - verlandhrim.duckdns.org - 9000 - PvE - Development - - https://discord.gg/HTnqGSwY2 - - - 59544be2-9435-400d-a589-30b2a39ed071 - Newfoundland - New Feb 2026. End of retail server. All Weclome (Free buffbot Yaraq) - ACE - rftc.duckdns.org - 9000 - PvE - Stable - - https://discord.gg/A5AHWZFUAa - - - 2d7bfb40-7726-4951-b65f-b4948807e602 - Conquest - Custom PvE server with live events, deep crafting, active development, and PK system. - ACE - ACConquest.ddns.net - 9000 - PvE - Development - https://dsc.gg/conquerac - https://discord.gg/Gsadhhv72S - - - b5bde7a5-537c-4665-8714-b7b097e73964 - NoESCapeGames - RetailLike with Modded NPCs to get you where you was on your last server. - ACE - noescapegames.com - 9000 - PvE - Development - http://noescapegames.com - https://discord.gg/46C8afEVs4 - - - 99b5533c-196a-4d38-8db1-dbad7a91d7a2 - Nexus - PvP/PvE hybrid server. Custom content, balanced combat,very active development. - ACE - 135.148.136.179 - 9000 - PvP - Stable - https://www.nexusserver.wiki/ - https://discord.gg/npZw7j6T - - - b846e003-c089-4ab1-b2b6-22937aee0cc4 - Leafdawning - A standard, no-frills PvE end-of-retail ACE Asheron's Call server. Runs on dedicated hardware with the ability to host a large number of players. Enforced 3 account limit. - ACE - leafdawning.duckdns.org - 9000 - PvE - Stable - - https://discord.gg/sQpDgAzSXB - - - a94b42cb-898c-4209-9246-82d21211aac1 - InfiniteLeaftide - Inifinite Level and Enlightenment, an Infinite Extended Retail Experience. Custom content, constant development and active community! - ACE - game.infiniteleaftide.online - 9000 - PvE - Development - - https://discord.gg/KtNxP8CaZt - - - ea2c553a-11a8-4e9c-a96a-5d32e57c3143 - FrostfACE - New accounts include lvl 275 starter characters. Retail character restoration also supported. - ACE - 172.111.230.127 - 9000 - PvE - Development - - - 3c431416-a5dc-4c15-9d9d-3262b64ed4e0 Drunkenfell @@ -108,18 +12,6 @@ http://www.drunkenfell.com https://discord.gg/e365fBcuaB - - a7502ac9-0cec-4caf-b81c-b3eb40a4689a - Reefcull - A PVE Server thats goal is to mimic end of game retail. - GDL - reefcull.gdleac.com - 9000 - PvE - Stable - - https://discord.gg/3ngsm6e - 394C58D0-885D-466B-B17F-D7E0B96FE3E2 Seedsow @@ -156,6 +48,18 @@ https://discord.gg/TtkcWbv + + ea2c553a-11a8-4e9c-a96a-5d32e57c3143 + FrostfACE + New accounts include lvl 275 starter characters. Retail character restoration also supported. + ACE + 172.111.230.127 + 9000 + PvE + Development + + + 26d9ec3d-9fbf-4fda-95f9-8d87e005ae3a Coldeve @@ -192,42 +96,6 @@ http://a-chard.ddns.net/ https://discord.gg/gmHfqt2J2D - - A14543EC-91B1-40D5-A9DE-F25806A0BFF7 - FunkyTown 2.0 - Retail with a little bit of Funky - GDL - funkytownac.com - 9050 - PvE - Stable - https://www.funkytownac.com/ - https://discord.gg/4gzFWTMu - - - 74C3F06D-1D0C-4BCA-83DE-2C8EE342DFD0 - FunkyTown PK - Retail with a little bit of Funky. PVP ONLY. Not moderated, No Warranties, No Refunds. - GDL - funkytownac.com - 9059 - PvP - Stable - https://www.funkytownac.com/ - https://discord.gg/CHgzDYVk - - - C6848B06-CA87-4CD1-B615-0C2A9781D01C - FunkyTEST - FunkyTown 2.0 Test Server - GDL - funkytownac.com - 9055 - PvE - Experimental - https://www.funkytownac.com/ - https://discord.gg/4gzFWTMu - bb791cc4-05c2-49ee-8473-9ab547cefce7 Frostcull @@ -240,6 +108,18 @@ https://discord.gg/RQF7HgEZn4 + + A14543EC-91B1-40D5-A9DE-F25806A0BFF7 + FunkyTown 2.0 + Retail with a little bit of Funky + GDL + funkytownac.com + 9050 + PvE + Stable + https://www.funkytownac.com/ + https://discord.gg/4gzFWTMu + 71eb9232-d22c-421b-a6ca-7b8c54821573 Asheron4Fun.com @@ -276,6 +156,18 @@ https://discord.gg/snv52pX + + a7502ac9-0cec-4caf-b81c-b3eb40a4689a + Reefcull + A PVE Server thats goal is to mimic end of game retail. + GDL + reefcull.gdleac.com + 9000 + PvE + Stable + + https://discord.gg/3ngsm6e + 26D70BC9-78E9-4DDC-BCFF-7D5B36F6AC17 GDLE Test @@ -324,6 +216,18 @@ https://derptide.net https://discord.gg/3gDTBVtfAg + + 0bc928ba-7f03-4052-8436-33fc6612add5 + Shadowland + A PVE Server thats goal is to mimic end of game retail. No max on accounts or sessions. + ACE + shadowland.zapto.org + 9000 + PvE + Stable + WIP + https://discord.gg/XwMFx9Jty2 + f023fbf9-caab-4745-8205-81c4d7c061ad Harvestbud @@ -337,16 +241,40 @@ https://discord.gg/jd3dEJf - 0bc928ba-7f03-4052-8436-33fc6612add5 - Shadowland - A PVE Server thats goal is to mimic end of game retail. No max on accounts or sessions. + 74C3F06D-1D0C-4BCA-83DE-2C8EE342DFD0 + FunkyTown PK + Retail with a little bit of Funky. PVP ONLY. Not moderated, No Warranties, No Refunds. + GDL + funkytownac.com + 9059 + PvP + Stable + https://www.funkytownac.com/ + https://discord.gg/CHgzDYVk + + + C6848B06-CA87-4CD1-B615-0C2A9781D01C + FunkyTEST + FunkyTown 2.0 Test Server + GDL + funkytownac.com + 9055 + PvE + Experimental + https://www.funkytownac.com/ + https://discord.gg/4gzFWTMu + + + 1528da56-f52c-4e84-947c-a814255c7a23 + Doctide + The place where PvP happens ACE - shadowland.zapto.org + doctide.online 9000 - PvE + PvP Stable - WIP - https://discord.gg/XwMFx9Jty2 + http://doctide.online + https://discord.gg/Qts4sF58H6 4db379b0-587a-4775-9b7f-879e0f15ba4b @@ -361,16 +289,16 @@ https://discord.gg/gTv9XMC8aN - 1528da56-f52c-4e84-947c-a814255c7a23 - Doctide - The place where PvP happens + a94b42cb-898c-4209-9246-82d21211aac1 + InfiniteLeaftide + Inifinite Level and Enlightenment, an Infinite Extended Retail Experience. Custom content, constant development and active community! ACE - doctide.online + game.infiniteleaftide.online 9000 - PvP - Stable - http://doctide.online - https://discord.gg/Qts4sF58H6 + PvE + Development + + https://discord.gg/KtNxP8CaZt 150340eb-349b-402e-90b0-4bc3c7e06d11 @@ -456,6 +384,18 @@ https://discord.gg/d3ZPpQsHCd + + b846e003-c089-4ab1-b2b6-22937aee0cc4 + Leafdawning + A standard, no-frills PvE end-of-retail ACE Asheron's Call server. Runs on dedicated hardware with the ability to host a large number of players. Enforced 3 account limit. + ACE + leafdawning.duckdns.org + 9000 + PvE + Stable + + https://discord.gg/sQpDgAzSXB + bb096062-b8f4-49ca-8472-433508a55b9d MorningStorm @@ -468,18 +408,6 @@ - - c4b49ae2-79fb-4687-86ab-4b9675cd9292 - Ebontide - A PvP server that will resemble end-of-retail. - ACE - ebontide.zapto.org - 9000 - PvP - Stable - - https://discord.gg/r4b9yeNY - b18f80c2-5c50-4e4a-828b-6278f71bfaca LeafDawn @@ -504,6 +432,42 @@ https://harvestagain.net/ https://discord.gg/znS865qNx8 + + c4b49ae2-79fb-4687-86ab-4b9675cd9292 + Ebontide + A PvP server that will resemble end-of-retail. + ACE + ebontide.zapto.org + 9000 + PvP + Stable + + https://discord.gg/r4b9yeNY + + + 99b5533c-196a-4d38-8db1-dbad7a91d7a2 + Nexus + PvP/PvE hybrid server. Custom content, balanced combat,very active development. + ACE + 135.148.136.179 + 9000 + PvP + Stable + https://www.nexusserver.wiki/ + https://discord.gg/npZw7j6T + + + b5bde7a5-537c-4665-8714-b7b097e73964 + NoESCapeGames + RetailLike with Modded NPCs to get you where you was on your last server. + ACE + noescapegames.com + 9000 + PvE + Development + http://noescapegames.com + https://discord.gg/46C8afEVs4 + 8bdfb181-b68e-4bcb-bd2b-656b6d997d41 Mistwood @@ -516,6 +480,30 @@ https://discord.gg/UMjW7qckpb + + 2d7bfb40-7726-4951-b65f-b4948807e602 + Conquest + Custom PvE server with live events, deep crafting, active development, and PK system. + ACE + ACConquest.ddns.net + 9000 + PvE + Development + https://dsc.gg/conquerac + https://discord.gg/Gsadhhv72S + + + 59544be2-9435-400d-a589-30b2a39ed071 + Newfoundland + New Feb 2026. End of retail server. All Weclome (Free buffbot Yaraq) + ACE + rftc.duckdns.org + 9000 + PvE + Stable + + https://discord.gg/A5AHWZFUAa + ff2ba4ee-aeaa-4870-92a7-3dc203e7e76c Eversong @@ -588,4 +576,16 @@ https://discord.gg/f4CM9yqGbF + + 7f3b9c21-4e8a-4d16-b2f7-1a9e5c0d3b84 + Verland_Hrim + PvE End of Retail. Quality of Life + Improved Society Rewards. 3 account limit. All Welcome. + ACE + verlandhrim.duckdns.org + 9000 + PvE + Development + + https://discord.gg/HTnqGSwY2 + diff --git a/scripts/reorder_servers.py b/scripts/reorder_servers.py new file mode 100644 index 0000000..498b046 --- /dev/null +++ b/scripts/reorder_servers.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Reorder Servers.xml by when each server was most recently added. + +A server's position comes from its latest "add": the commit in which its + first appeared in the file, or in which it reappeared after being absent +from the previous version. Within a commit, servers keep the order they had +in that commit's file. + +Only the order of the blocks changes. Each block's bytes and the +whitespace between blocks stay where they are. + +Usage +----- +reorder_servers.py + +Run from inside the git repository. The file is rewritten in place. +""" + +import re +import subprocess +import sys +import xml.etree.ElementTree as ET +from collections import Counter + +ITEM_RE = re.compile(r".*?", re.DOTALL) +ID_RE = re.compile(r"\s*([^<]*?)\s*") + + +def ids_in_order(xml_text): + """Return the server ids in document order. + + Parameters + ---------- + xml_text : str + A Servers.xml document, or a single ```` fragment. + + Returns + ------- + list of str + The stripped ```` text of each ``ServerItem``. + """ + root = ET.fromstring(xml_text) + return [item.find("id").text.strip() for item in root.iter("ServerItem")] + + +def latest_add_positions(versions): + """Return each server's position from its latest add. + + Parameters + ---------- + versions : list of list of str + The server ids of each committed version of the file, oldest first. + + Returns + ------- + dict of str to tuple of (int, int) + Maps each id to ``(commit_index, position_in_that_commit)`` for the + commit where it was most recently added. + """ + positions = {} + previous = set() + for commit_index, ids in enumerate(versions): + for position, server_id in enumerate(ids): + if server_id not in previous: + positions[server_id] = (commit_index, position) + previous = set(ids) + return positions + + +def ids_by_commit(path, cwd=None): + """Return the server ids of each commit that touched ``path``, oldest first. + + Ids are read with a regular expression rather than an XML parser because + some historical versions of the file are not well-formed XML. Skipping + them would misattribute the servers added in those commits. + + Parameters + ---------- + path : str + Path of the file, relative to the repository root. + cwd : str, optional + Repository directory. Defaults to the current directory. + + Returns + ------- + list of list of str + One id list per commit, in document order. + + Raises + ------ + subprocess.CalledProcessError + If git fails, e.g. if a commit does not contain the file. + """ + shas = subprocess.run( + ["git", "log", "--reverse", "--format=%H", "--", path], + cwd=cwd, capture_output=True, text=True, check=True, + ).stdout.split() + versions = [] + for sha in shas: + content = subprocess.run( + ["git", "show", f"{sha}:{path}"], + cwd=cwd, capture_output=True, text=True, check=True, + ).stdout + versions.append(ID_RE.findall(content)) + return versions + + +def reorder_text(text, positions): + """Return ``text`` with its ```` blocks sorted by ``positions``. + + Parameters + ---------- + text : str + A Servers.xml document. + positions : dict of str to tuple + Sort key for each server id, as returned by `latest_add_positions`. + + Returns + ------- + str + The reordered document. Block contents are preserved exactly. The + separators between blocks are replaced with the most common separator + in the input, so indentation is consistent in the output. + + Raises + ------ + ValueError + If the document has no servers, or a server has no position. + """ + matches = list(ITEM_RE.finditer(text)) + if not matches: + raise ValueError("no ServerItem blocks found") + + blocks = [] + for match in matches: + (server_id,) = ids_in_order(match.group(0)) + if server_id not in positions: + raise ValueError(f"no add history for server {server_id}") + blocks.append((positions[server_id], match.group(0))) + + separators = [ + text[a.end():b.start()] for a, b in zip(matches, matches[1:]) + ] + if separators: + dominant = Counter(separators).most_common(1)[0][0] + separators = [dominant] * len(separators) + ordered = [block for _, block in sorted(blocks, key=lambda pair: pair[0])] + + out = [text[: matches[0].start()]] + for index, block in enumerate(ordered): + out.append(block) + if index < len(separators): + out.append(separators[index]) + out.append(text[matches[-1].end():]) + return "".join(out) + + +def main(argv): + """Reorder the file given on the command line. + + Parameters + ---------- + argv : list of str + Command-line arguments, where ``argv[0]`` is the program name. + + Returns + ------- + int + 0 on success, 2 on bad arguments or unusable input. + """ + if len(argv) != 2: + print("usage: reorder_servers.py ", file=sys.stderr) + return 2 + + path = argv[1] + with open(path, encoding="utf-8", newline="") as f: + text = f.read() + + try: + positions = latest_add_positions(ids_by_commit(path)) + reordered = reorder_text(text, positions) + except (ValueError, subprocess.CalledProcessError, ET.ParseError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + + with open(path, "w", encoding="utf-8", newline="") as f: + f.write(reordered) + + print(f"Reordered {len(ITEM_RE.findall(text))} servers in {path}.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/test_reorder_servers.py b/scripts/test_reorder_servers.py new file mode 100644 index 0000000..c2c84e5 --- /dev/null +++ b/scripts/test_reorder_servers.py @@ -0,0 +1,155 @@ +import os +import subprocess +import tempfile +import unittest + +import reorder_servers as rs + + +def item(server_id, name="Name"): + return ( + f"\n" + f" {server_id}\n" + f" {name}\n" + f" " + ) + + +def document(*items, separator="\n "): + body = separator.join(items) + return ( + '\n' + "\n " + f"{body}\n" + "\n" + ) + + +class LatestAddPositionsTests(unittest.TestCase): + def test_first_appearance_is_recorded(self): + positions = rs.latest_add_positions([["a", "b"], ["a", "b", "c"]]) + self.assertEqual(positions, {"a": (0, 0), "b": (0, 1), "c": (1, 2)}) + + def test_bulk_add_in_one_commit_keeps_file_order(self): + positions = rs.latest_add_positions([["x", "y", "z"]]) + self.assertLess(positions["x"], positions["y"]) + self.assertLess(positions["y"], positions["z"]) + + def test_removal_and_readd_resets_position(self): + positions = rs.latest_add_positions([["a", "b"], ["b"], ["b", "a"]]) + self.assertEqual(positions["a"], (2, 1)) + self.assertEqual(positions["b"], (0, 1)) + + def test_edits_do_not_change_position(self): + # Same ids in later commits (e.g. description edits) add nothing new. + positions = rs.latest_add_positions([["a"], ["a"], ["a"]]) + self.assertEqual(positions, {"a": (0, 0)}) + + def test_reordering_within_history_does_not_count_as_add(self): + positions = rs.latest_add_positions([["a", "b"], ["b", "a"]]) + self.assertEqual(positions, {"a": (0, 0), "b": (0, 1)}) + + +class ReorderTextTests(unittest.TestCase): + def test_blocks_are_sorted_by_position(self): + text = document(item("b"), item("a"), item("c")) + positions = {"a": (0, 0), "b": (0, 1), "c": (0, 2)} + result = rs.reorder_text(text, positions) + self.assertEqual(rs.ids_in_order(result), ["a", "b", "c"]) + + def test_block_contents_are_preserved_byte_for_byte(self): + original_blocks = [item("b", "Bee"), item("a", "Ay")] + text = document(*original_blocks) + positions = {"a": (0, 0), "b": (1, 0)} + result = rs.reorder_text(text, positions) + self.assertEqual(result, document(original_blocks[1], original_blocks[0])) + + def test_separators_are_normalized_to_dominant_indentation(self): + text = document(item("b"), item("a"), item("c"), item("d")) + # One odd separator (4 spaces) before c; the other separators use 2. + text = text.replace("\n \n c", "\n \n c", 1) + positions = {"a": (0, 0), "b": (0, 1), "c": (0, 2), "d": (0, 3)} + result = rs.reorder_text(text, positions) + self.assertEqual(result, document(item("a"), item("b"), item("c"), item("d"))) + + def test_header_footer_and_separators_stay_put(self): + text = document(item("b"), item("a"), separator="\n\n ") + positions = {"a": (0, 0), "b": (1, 0)} + result = rs.reorder_text(text, positions) + self.assertTrue(result.startswith('\n\n ')) + self.assertTrue(result.endswith("\n\n")) + self.assertIn("\n\n ", result) + + def test_reorder_is_a_fixed_point(self): + text = document(item("c"), item("a"), item("b")) + positions = {"a": (0, 0), "b": (0, 1), "c": (0, 2)} + once = rs.reorder_text(text, positions) + twice = rs.reorder_text(once, positions) + self.assertEqual(once, twice) + + def test_missing_position_raises(self): + text = document(item("a"), item("unknown")) + with self.assertRaises(ValueError): + rs.reorder_text(text, {"a": (0, 0)}) + + def test_empty_document_raises(self): + with self.assertRaises(ValueError): + rs.reorder_text('', {}) + + +class IdsByCommitTests(unittest.TestCase): + """Uses a real git repository so the history reading is exercised end to end.""" + + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.repo = self._tmp.name + self.git("init", "-q") + self.git("config", "user.email", "test@example.com") + self.git("config", "user.name", "Test") + # Isolate from global git settings such as GPG signing and hooks. + self.git("config", "commit.gpgsign", "false") + self.git("config", "core.hooksPath", os.devnull) + + def git(self, *args): + return subprocess.run( + ["git", *args], cwd=self.repo, check=True, capture_output=True, text=True + ).stdout + + def commit_text(self, message, text): + path = os.path.join(self.repo, "Servers.xml") + with open(path, "w", encoding="utf-8") as f: + f.write(text) + self.git("add", "Servers.xml") + self.git("commit", "-q", "-m", message) + + def commit(self, message, *server_ids): + self.commit_text(message, document(*(item(i) for i in server_ids))) + + def history(self): + return rs.ids_by_commit("Servers.xml", cwd=self.repo) + + def test_reads_each_commit_oldest_first(self): + self.commit("first", "a", "b") + self.commit("second", "a", "b", "c") + self.commit("third", "c", "a") + self.assertEqual(self.history(), [["a", "b"], ["a", "b", "c"], ["c", "a"]]) + + def test_malformed_xml_commit_is_still_counted(self): + # Some real history has invalid XML; its servers must not be skipped. + self.commit("first", "a") + self.commit_text("malformed", document(item("b")).replace("", "