diff --git a/Directory.Build.props b/Directory.Build.props index 2385b72..5326804 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,28 @@ enable true false - true + + false true Harry Cordewener MUIndex diff --git a/content/reference/de/client-atlantis.md b/content/reference/de/client-atlantis.md new file mode 100644 index 0000000..00348a8 --- /dev/null +++ b/content/reference/de/client-atlantis.md @@ -0,0 +1,48 @@ +--- +kind: client +slug: atlantis +title: Atlantis +summary: Ein Client nur für macOS, langlebig und lange in der Beta. Sein Skripting ist als nicht mehr funktionierend dokumentiert, und das ist das eine ehrliche „Nein“ in diesem Abschnitt. +home: https://www.riverdark.net/atlantis/ +platform: macOS +capability: screen reader | unknown | +capability: TLS | yes | https://www.riverdark.net/atlantis/history.php +capability: UTF-8 | yes | https://www.riverdark.net/atlantis/history.php +capability: MCCP | yes | https://www.riverdark.net/atlantis/history.php +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | no | https://www.riverdark.net/atlantis/ +see-also: clients/mudlet +see-also: protocols/charset +--- + +Atlantis ist ein nativer macOS-Client, den es seit Mac OS X 10.3 gibt und der in der Catalina-Zeit +auf 64 Bit aktualisiert wurde. Er beherrscht die Zeichensatz-Aushandlung nach RFC 2066 und Unicode, +was besser ist, als sein Alter vermuten ließe, und er kann MCCP und SSL. + +## Das eine „Nein“ in diesem Abschnitt + +Sein Skripting war Perl über die CamelBones-Brücke, und die Startseite des Projekts selbst sagt, +dass es nicht mehr funktioniert — Apples Umgang mit Perl hat sich geändert, und der Autor der +Bibliothek ist vor einigen Jahren gestorben. Das ist ein *belegtes Fehlen*, und das ist etwas +anderes als ein Unbekannt; es ist die einzige Zelle im ganzen Client-Abschnitt, die so etwas trägt. +Überall sonst lautete die ehrliche Antwort, dass wir es nicht feststellen konnten. + +## Alles, was wir nicht feststellen konnten + +Die Versionsgeschichte ist vollständig und öffentlich und nennt **MCCP**, **SSL** und +**Zeichensatz-Aushandlung** — und nennt nie GMCP, MSDP, ATCP oder MSP. MXP taucht einmal auf, als +etwas, das für eine Version nach 1.0.0 vorgesehen war, die nicht gekommen ist. + +In der Skripting-API gibt es einen Perl-Aufruf `Atlantis::Speak()`, und es wäre leicht, das als +Screenreader-Unterstützung zu lesen. Das ist es nicht: Es ist ein per Skript ausgelöster Aufruf zur +Sprachausgabe in einem Skriptsystem, von dem das Projekt sagt, dass es nicht funktioniert. +VoiceOver, „accessible“ und „screen reader“ kommen weder auf der Startseite noch auf der +Downloadseite noch in der vollständigen Versionsgeschichte noch im archivierten Benutzerhandbuch +vor. + +Der aktuelle Download ist 0.9.9.8, nominell immer noch eine Beta, ohne dass irgendwo auf der Website +ein Veröffentlichungsdatum stünde. diff --git a/content/reference/de/client-beipmu.md b/content/reference/de/client-beipmu.md new file mode 100644 index 0000000..d9e12ba --- /dev/null +++ b/content/reference/de/client-beipmu.md @@ -0,0 +1,47 @@ +--- +kind: client +slug: beipmu +title: BeipMU +summary: Ein Windows-Client für die MUSH-Seite des Hobbys, mit Screenreader-Unterstützung im Ausgabefenster und Pueblo statt MXP. +home: https://beipdev.github.io/BeipMU/ +platform: Windows +capability: screen reader | yes | https://github.com/BeipDev/BeipMU/blob/master/Assets/Changes.txt +capability: TLS | yes | https://beipdev.github.io/BeipMU/ +capability: UTF-8 | yes | https://beipdev.github.io/BeipMU/ +capability: MCCP | unknown | +capability: GMCP | yes | https://github.com/BeipDev/BeipMU/blob/master/Documentation/GMCP.md +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://beipdev.github.io/BeipMU/ +see-also: clients/mushclient +see-also: clients/potato +see-also: collaborative-roleplay +--- + +BeipMU ist ein Windows-Client unter MIT-Lizenz, aktiv veröffentlicht, und einer der wenigen, die mit +Blick auf MUSH-artiges Spiel gebaut sind statt auf Kampf-MUDs — mehrere Eingabefenster, +Spawn-Fenster und eine Textmaschine, die lange Absätze erwartet. Skripting ist standardmäßig +JavaScript, weitere ActiveScript-Engines sind verfügbar. + +## Barrierefreiheit + +Das Ausgabefenster implementiert die Windows-Schnittstelle `IAccessible`, bewusst als Schritt hin zu +einer Benutzbarkeit für sehbehinderte Spieler eingebaut, und es gibt eine Trigger-Aktion **Speak** +für Sprachausgabe. Nirgends wird ein bestimmter Screenreader genannt, und ein Kapitel zur +Barrierefreiheit gibt es in der Dokumentation nicht. + +Eine Warnung, falls Sie nachsehen: Eine Seite in der projekteigenen Dokumentation sagt immer noch, +BeipMU könne keine Sprachsynthese nutzen. Diese Seite ist veraltet — das Änderungsprotokoll und die +Issue-Kommentare des Betreuers selbst sind beide jünger. + +## Zwei leichte Irrtümer über diesen Client + +**BeipMU implementiert MCMP, nicht MSP.** Das sind verschiedene Protokolle mit ähnlichen Namen und +ähnlichen Zwecken, und das eine als das andere zu lesen hieße, eine Behauptung in diese Tabelle zu +setzen, die niemand aufgestellt hat. Die MSP-Zeile sagt deshalb unbekannt. + +**Es unterstützt Pueblo, nicht MXP.** Pueblo ist das ältere Verfahren für HTML in einem MUD und MXP +das spätere; BeipMU dokumentiert einfache Pueblo-Stile und anklickbare Links. Zu MXP ließ sich weder +das eine noch das andere feststellen. diff --git a/content/reference/de/client-blightmud.md b/content/reference/de/client-blightmud.md new file mode 100644 index 0000000..d3a07ea --- /dev/null +++ b/content/reference/de/client-blightmud.md @@ -0,0 +1,51 @@ +--- +kind: client +slug: blightmud +title: Blightmud +summary: Ein moderner Terminal-Client in Rust, mit Lua-Skripting, eingebauter Sprachausgabe und einem Screenreader-Modus, der sich beim Server selbst ankündigt. +home: https://github.com/Blightmud/Blightmud +platform: Linux +platform: macOS +platform: Windows (WSL only) +capability: screen reader | yes | https://github.com/Blightmud/Blightmud +capability: TLS | yes | https://github.com/Blightmud/Blightmud +capability: UTF-8 | yes | https://github.com/Blightmud/Blightmud +capability: MCCP | yes | https://github.com/Blightmud/Blightmud +capability: GMCP | yes | https://github.com/Blightmud/Blightmud +capability: MSDP | yes | https://github.com/Blightmud/Blightmud +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/Blightmud/Blightmud +see-also: clients/tintin +see-also: clients/mudlet +see-also: protocols/ttype +--- + +Blightmud ist ein Terminal-Client in Rust, GPL 3, und einer der am aktivsten veröffentlichten +Clients in diesem Abschnitt. Skripting ist Lua. Er läuft nur im Terminal: Es gibt keinen nativen +Windows-Build, und Windows-Nutzer betreiben ihn unter WSL. + +## Barrierefreiheit + +Blightmud hat hier drei verschiedene Stücke, was mehr ist, als eine einzelne Zeile tragen kann: + +- Einen **screenreader-freundlichen Modus** (`--reader-mode` oder die Einstellung `reader_mode`), + der die Terminal-Oberfläche in etwas verwandelt, dem ein Screenreader folgen kann. Den + Statusbereich unterstützt er nicht. +- **Eingebaute Sprachausgabe**, als optionale Kompilierung, mit einer Lua-API, die ein Skript + benutzen kann — darunter ein `tts.gag()`, um eine passende Zeile vom Vorlesen auszunehmen. Die + Dokumentation ist offen darin, dass die eigene Sprachausgabe zusammen mit einem Screenreader nicht + immer eine glückliche Verbindung ist. +- **Automatische MTTS-Ankündigung**: Im Reader-Modus oder bei aktivierter Sprachausgabe fügt er + `MTTS_SCREEN_READER` zu dem hinzu, was er dem Server über sich selbst mitteilt, damit ein Spiel, + dem das wichtig ist, sich anpassen kann. + +Wie bei TinTin++ wird kein bestimmter Screenreader genannt, das ist also ein dokumentierter Modus +und keine geprüfte Verträglichkeit mit einem Produkt. + +## Wo die Tabelle unbekannt sagt + +**MXP**, **MSP** und **ATCP** kommen weder in der README des Projekts noch in seiner mitgelieferten +Hilfe vor. **MCCP** ist als v2 dokumentiert; ob auch v1 behandelt wird, haben wir nicht +festgestellt. diff --git a/content/reference/de/client-mudlet.md b/content/reference/de/client-mudlet.md new file mode 100644 index 0000000..542bc5a --- /dev/null +++ b/content/reference/de/client-mudlet.md @@ -0,0 +1,57 @@ +--- +kind: client +slug: mudlet +title: Mudlet +summary: Plattformübergreifend, mit Lua skriptbar und der Client mit der am gründlichsten dokumentierten Screenreader-Unterstützung in diesem Abschnitt. +home: https://www.mudlet.org/ +platform: Windows +platform: macOS +platform: Linux +capability: screen reader | yes | https://wiki.mudlet.org/w/Manual:Screen_Readers +capability: TLS | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: UTF-8 | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MCCP | unknown | +capability: GMCP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MSDP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: ATCP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MXP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MSP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: scripting | yes | https://github.com/Mudlet/Mudlet +see-also: clients/blightmud +see-also: clients/tintin +see-also: protocols/gmcp +see-also: connecting +--- + +Mudlet ist ein grafischer Client mit Kartenwerkzeug, einem Paketsystem und einer Lua-API, gegen die +der größte Teil seines eigenen Funktionsumfangs geschrieben ist. Er steht unter der GPL, wird aktiv +veröffentlicht und ist die übliche Empfehlung für alle, die mit einem modernen Kampf-MUD anfangen. + +## Barrierefreiheit + +Das ist der Client mit dem stärksten dokumentierten Fall in diesem Abschnitt, und es lohnt sich +auszubuchstabieren, was „dokumentiert“ hier heißt, denn es ist ungewöhnlich. + +Mudlet hat ein **Handbuchkapitel zu Screenreadern**, Seiten je Betriebssystem, die Narrator, NVDA +und JAWS unter Windows, Orca unter Linux und VoiceOver unter macOS nennen, einen Befehl +`mudlet access on` im Client und eine Option, eingehenden Spieltext über den Screenreader ansagen zu +lassen. Es gibt außerdem eine Einstellung, die dem Server über MTTS die Nutzung eines Screenreaders +mitteilt, damit ein Spiel sich anpassen kann, wenn es will. + +Es ist auch offen darin, wo es nicht gut funktioniert: Die eigene Windows-Seite sagt, dass JAWS das +Ausgabefenster nicht so vorliest wie andere Screenreader, und empfiehlt stattdessen Narrator oder +NVDA. Ein Projekt, das den Fall veröffentlicht, in dem seine Unterstützung für Barrierefreiheit +untauglich ist, gibt Ihnen bessere Auskunft als eines, das ein Häkchen veröffentlicht. + +## Wo die Tabelle unbekannt sagt + +**MCCP.** Mudlets Quelltext implementiert MCCP v1 und v2, aber die Seite des Handbuchs zu den +unterstützten Protokollen führt es nicht auf, und die Regel dieses Abschnitts lautet, dass eine +Fähigkeitsaussage die Dokumentation des Projekts selbst zitiert. Eine Konstante aus einer +Header-Datei zu lesen ist nicht derselbe Vorgang, also sagt die Zelle unbekannt. + +## Hinweis zur Kodierung + +Mudlets voreingestellte Kodierung für Serverdaten ist ASCII und nicht UTF-8, und die +CHARSET-Aushandlung kam in 4.10 hinzu. Wenn der Text eines Spiels in einem frischen Profil falsch +herauskommt, ist diese Einstellung die erste Stelle, an der man nachsieht. diff --git a/content/reference/de/client-mushclient.md b/content/reference/de/client-mushclient.md new file mode 100644 index 0000000..242f8a1 --- /dev/null +++ b/content/reference/de/client-mushclient.md @@ -0,0 +1,53 @@ +--- +kind: client +slug: mushclient +title: MUSHclient +summary: Der seit Langem etablierte Windows-Client. Fünf Skriptsprachen, eine Plugin-Architektur, in der der Großteil seiner Protokollunterstützung lebt, und eine Veröffentlichungsgeschichte, die sich verlangsamt hat. +home: https://www.mushclient.com/ +platform: Windows +platform: Linux (Wine) +capability: screen reader | unknown | +capability: TLS | unknown | +capability: UTF-8 | unknown | +capability: MCCP | yes | https://www.mushclient.com/mushclient/mccp.htm +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | yes | https://www.mushclient.com/gmcp +capability: MXP | yes | https://www.mushclient.com/mushclient/doc/general/features.html +capability: MSP | yes | https://github.com/nickgammon/mushclient/blob/master/plugins/msp.xml +capability: scripting | yes | https://www.mushclient.com/mushclient/doc/general/features.html +see-also: clients/mudlet +see-also: clients/potato +see-also: protocols/mccp +--- + +MUSHclient ist Nick Gammons Windows-Client, MIT-lizenziert, und über eine lange Strecke die +Standardantwort für alle unter Windows. Er skriptet in Lua, VBScript, JScript, PerlScript und +Python, und vieles von dem, was er tut, tragen Plugins statt des Kerns — was eine echte +architektonische Entscheidung ist und zugleich der Grund, warum sich mehrere Zeilen oben schwerer +beantworten lassen, als sie aussehen. + +Die letzte getaggte Veröffentlichung ist **5.06 vom März 2019**. Ins Repository wird weiterhin +committet, und es gibt Release Notes für ein 5.07, das nicht ausgeliefert wurde. + +## Warum so viele Zeilen unbekannt sagen + +Bei jeder einzelnen davon lautet die ehrliche Antwort „wir konnten es nicht feststellen“, und die +Gründe sind verschieden: + +- **GMCP** — die projekteigene Seite dazu zeigt ein *Beispiel*-Plugin, das man schreiben könnte, + keine Funktion, die der Client hat. Das ist etwas anderes als ausgelieferte Unterstützung, also + ist die Zelle unbekannt statt ja. +- **TLS** — der dokumentierte Weg ist ein externer `stunnel`-Prozess. Ein Commit, der TLS über + OpenSSL hinzufügt, ist 2026 im master-Branch gelandet und in keiner Veröffentlichung enthalten, es + gibt also nichts, was heute jemand installieren könnte und worauf wir zeigen könnten. +- **UTF-8** — die CHARSET-Aushandlung taucht in den Notizen zum unveröffentlichten 5.07 auf und + nirgends, wo wir sie in der Dokumentation einer ausgelieferten Version hätten finden können. +- **MSDP** — nichts in die eine oder andere Richtung. +- **Screenreader** — ein Plugin für Sprachausgabe über Windows SAPI wird mit dem Client + ausgeliefert, und das ist nicht dasselbe wie Screenreader-Unterstützung. Im Handbuch gibt es + keinen Abschnitt zur Barrierefreiheit, und der Autor hat in seinem eigenen Forum beschrieben, + warum das Ausgabefenster für einen Screenreader schwer zu handhaben ist: Es kennt keine aktuelle + Zeile. Wir konnten keine Antwort feststellen, also gibt die Tabelle keine. + +Keines davon ist ein *Nein*. Mehrere sind vermutlich ein Ja, und wir konnten es nicht zeigen. diff --git a/content/reference/de/client-potato.md b/content/reference/de/client-potato.md new file mode 100644 index 0000000..da043d5 --- /dev/null +++ b/content/reference/de/client-potato.md @@ -0,0 +1,48 @@ +--- +kind: client +slug: potato +title: Potato MUSHclient +summary: Ein plattformübergreifender Tcl/Tk-Client, geschrieben für MUSH-Spieler. Gute Unterstützung für Kodierungen und eine Dokumentation, die zu den meisten Protokollen überhaupt nichts sagt. +home: https://www.potatomushclient.com/ +platform: Windows +platform: Linux +platform: macOS +capability: screen reader | unknown | +capability: TLS | yes | https://github.com/potatomushclient/potato/wiki/ConfigureWorldsBasics +capability: UTF-8 | yes | https://github.com/potatomushclient/potato/wiki/Features +capability: MCCP | unknown | +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/potatomushclient/potato/wiki/FAQs +see-also: clients/beipmu +see-also: clients/mushclient +see-also: collaborative-roleplay +--- + +Potato ist ein Tcl/Tk-Client für das MUSH-Spiel — mehrere Welten, Spawn-Fenster und ein Satz von +Voreinstellungen, die davon ausgehen, dass Sie Posen tippen und keine Kampfbefehle. Er läuft aus +derselben Quelle unter Windows, Linux und macOS, wobei die macOS-Builds meist ein bis zwei Versionen +zurückliegen. + +Er handelt die Zeichenkodierung aus und spricht volles Unicode, was für die MUSH-Seite des Hobbys +die Fähigkeit ist, auf die es in der Praxis am meisten ankommt. + +Beachten Sie eine dokumentierte Einschränkung: Er unterstützt die Verbindung zu einem Port, der von +Beginn an SSL spricht, und seine eigene Konfigurationsseite sagt, dass ausgehandeltes SSL nach Art +von STARTTLS **nicht** unterstützt wird. + +## Warum sechs Zeilen unbekannt sagen + +Wir haben die Startseite des Projekts, seine Downloadseite, alle 103 Hilfedateien seines Wikis und +seinen gesamten Quellbaum nach GMCP, MSDP, MCCP, MXP, MSP und ATCP durchsucht. Zu keinem davon gibt +es eine dokumentierte Aussage. Es gibt *Code*, der einige davon berührt, und dieser Abschnitt macht +aus Code keine Fähigkeitsaussage — eine Tabelle, die auf Grundlage einer Konstante in einer +Header-Datei „ja“ sagt, gibt ein Versprechen ab, das das Projekt nie gegeben hat. + +Die Screenreader-Zeile ist dieselbe Antwort, auf demselben Weg erreicht: Eine Suche ohne Beachtung +der Groß- und Kleinschreibung nach „screen reader“, „text-to-speech“, NVDA, JAWS, VoiceOver, +„accessibility“, „visually impaired“ und „blind“ über alles, was das Projekt veröffentlicht, ergab +überhaupt nichts. Das ist kein Befund über die Software. diff --git a/content/reference/de/client-tintin.md b/content/reference/de/client-tintin.md new file mode 100644 index 0000000..f1f886a --- /dev/null +++ b/content/reference/de/client-tintin.md @@ -0,0 +1,53 @@ +--- +kind: client +slug: tintin +title: TinTin++ +summary: Ein Terminal-Client mit eigener Skriptsprache, auf jeder Plattform einschließlich Telefonen, und mit einem dokumentierten Screenreader-Modus. +home: https://tintin.mudhalla.net/ +platform: Linux +platform: macOS +platform: Windows +platform: Android +platform: iOS +capability: screen reader | yes | https://tintin.mudhalla.net/manual/screen_reader.php +capability: TLS | yes | https://github.com/scandum/tintin +capability: UTF-8 | yes | https://github.com/scandum/tintin +capability: MCCP | yes | https://tintin.mudhalla.net/ +capability: GMCP | yes | https://tintin.mudhalla.net/manual/event.php +capability: MSDP | yes | https://tintin.mudhalla.net/manual/msdp.php +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/scandum/tintin +see-also: clients/blightmud +see-also: clients/mudlet +see-also: protocols/msdp +see-also: protocols/ttype +--- + +TinTin++ ist ein Kommandozeilen-Client, GPL 3, aktiv veröffentlicht, und er läuft an mehr Orten als +alles andere hier — einschließlich Android und iOS. Seine Skriptsprache ist seine eigene, knapp und +zu sehr viel fähig; ein erheblicher Teil dessen, was andere Clients in der Oberfläche machen, ist +hier eine `#config`-Zeile. + +Derselbe Autor pflegt die Protokollspezifikationen für **MSSP** und **MSDP**, weshalb so viele der +Protokollseiten in diesem Abschnitt dieselbe Website zitieren. + +## Barrierefreiheit + +TinTin++ hat eine eigene Handbuchseite zum **Screenreader-Modus** (`#config screen reader on` oder +`-s` beim Start). Ihn einzuschalten bewirkt zweierlei: Es entfernt oder verändert visuelle Elemente, +die vorgelesen keinen Sinn ergeben, und es meldet dem Server die Nutzung eines Screenreaders über +[MTTS](/reference/protocols/ttype), damit ein Spiel seine eigene Ausgabe anpassen kann. + +Das ist ein dokumentierter Modus und keine Aussage über einen Test mit einem bestimmten Screenreader +— auf der Seite wird kein Produkt genannt. Das ist deutlich schwächer als ein Client, der die +Screenreader benennt, mit denen er funktioniert, und deutlich stärker als nichts. + +## Wo die Tabelle unbekannt sagt + +Zu **MXP** und **MSP** gibt es beide Male Community-Skripte auf der Website des Projekts, und ein +Skript ist nicht dasselbe wie ein Client, der ein Protokoll unterstützt — das MXP-Skript sagt +rundheraus, dass es womöglich nicht auf jedem MUD funktioniert. Native Unterstützung für eines von +beiden ließ sich nicht feststellen. Zu **ATCP** haben wir weder das eine noch das andere gefunden; +zu beachten ist, dass ATCP weitgehend von GMCP abgelöst ist, das TinTin++ sehr wohl unterstützt. diff --git a/content/reference/de/client-tinyfugue.md b/content/reference/de/client-tinyfugue.md new file mode 100644 index 0000000..49c98d3 --- /dev/null +++ b/content/reference/de/client-tinyfugue.md @@ -0,0 +1,46 @@ +--- +kind: client +slug: tinyfugue +title: TinyFugue +summary: Der klassische UNIX-Terminal-Client. Upstream hat seit 2007 nichts mehr veröffentlicht; ein gepflegter Fork führt ihn weiter. +home: https://tinyfugue.sourceforge.net/ +platform: Linux +platform: macOS +platform: BSD +capability: screen reader | unknown | +capability: TLS | yes | https://tinyfugue.sourceforge.net/ +capability: UTF-8 | unknown | +capability: MCCP | yes | https://tinyfugue.sourceforge.net/ +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://tinyfugue.sourceforge.net/ +see-also: clients/tintin +see-also: clients/blightmud +--- + +TinyFugue — „tf“ — ist der Terminal-Client, den ein großer Teil der MUSH-Welt zwei Jahrzehnte lang +benutzt hat, mit getrennten Bereichen für Eingabe und Ausgabe, einer eigenen Makrosprache und einem +Satz von Gewohnheiten, die mehrere seiner Konkurrenten überlebt haben. + +**Upstream ruht**: Die letzte Veröffentlichung ist 5.0 Beta 8 vom Januar 2007. Es baut noch immer, +und es funktioniert noch immer. + +Ein gepflegter Fork, *TinyFugue Rebirth*, wird aktiv veröffentlicht und ergänzt GMCP, ATCP, +Unterstützung für Breitzeichen über ICU sowie Python- und Lua-Skripting neben der eigenen +Makrosprache. Die Tabelle oben beschreibt **Upstream**, denn dorthin führt „TinyFugue“; wenn Sie +heute etwas installieren, lohnt sich zuerst ein Blick auf den Fork. + +## Die Falle in der Dokumentation dieses Clients + +Upstream hat ein Dokumentationsthema namens **„non-visual mode“**. Darin geht es nicht um +Hilfstechnik — es geht darum, die Eingabe auf die unterste Zeile zu beschränken —, und es erwähnt +nirgends einen Screenreader, keine Sprachausgabe und keine blinden Nutzer. Eine Fähigkeitstabelle, +die per Stichwortsuche zusammengestellt wird, würde aus diesem Dateinamen ein Ja machen. Diese hier +sagt unbekannt, denn das ist es, was die Dokumentation hergibt. + +UTF-8 ist eine Antwort derselben Form: Die dokumentierte Unterstützung für Kodierungen betrifft +8-Bit-Zeichensätze nach ISO 8859, und wir haben von Upstream keine Aussage zu UTF-8 gefunden, weder +in die eine noch in die andere Richtung. diff --git a/content/reference/de/client-vipmud.md b/content/reference/de/client-vipmud.md new file mode 100644 index 0000000..29db37a --- /dev/null +++ b/content/reference/de/client-vipmud.md @@ -0,0 +1,44 @@ +--- +kind: client +slug: vipmud +title: VIP Mud +summary: Ein kommerzieller Windows-Client, von Grund auf für blinde Spieler gebaut. Er nennt sieben Screenreader — und veröffentlicht fast nichts über seine Protokollunterstützung. +home: https://www.gmagames.com/vipmud.shtml +platform: Windows +capability: screen reader | yes | https://www.gmagames.com/vipmud.shtml +capability: TLS | unknown | +capability: UTF-8 | unknown | +capability: MCCP | unknown | +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | yes | https://www.gmagames.com/vipmud.shtml +capability: scripting | yes | https://www.gmagames.com/vipmud.shtml +see-also: clients/mudlet +see-also: clients/blightmud +--- + +VIP Mud ist der eine Client in diesem Abschnitt, dessen *gesamte* Entwurfsprämisse Barrierefreiheit +ist. Er ist kommerziell — dreißig Dollar, mit einer dreißigtägigen Vollversion zum Testen, nach der +er mit eingeschränktem Funktionsumfang weiterläuft — und er ist ein Windows-Programm. + +Er ist mit Abstand die stärkste Aussage zur Barrierefreiheit hier, und ungewöhnlicherweise ist sie +konkret. Die Produktseite nennt **JAWS, Window-Eyes, System Access, NVDA, Cobra, SuperNova/Hal und +Microsoft SAPI** als von Haus aus funktionierend und beschreibt Funktionen, die nur Sinn ergeben, +wenn man scharf über das Problem nachgedacht hat: verschiedene Stimmen je Fenster und je Art der +Ausgabe, das Ausblenden von Spam aus der Sprachausgabe bei weiterhin sichtbarem Text und mehrere +Methoden, ASCII-Kunst zu unterdrücken — das Feindseligste, was ein MUD einem Screenreader schicken +kann. + +## Warum der Rest der Tabelle leer ist + +Weil der Anbieter eine Werbeseite veröffentlicht und kein Handbuch. Nichts darauf erwähnt GMCP, +MSDP, MCCP, MXP, ATCP, TLS oder Zeichenkodierung; es beschreibt das Produkt als „a Telnet-based +client“ und belässt es dabei. **Neun Unbekannt in Folge sind kein Urteil über die Software.** So +sieht eine Matrix aus, wenn die einzige verfügbare Quelle eine einzige Seite ist, und sie als neun +Nein zu veröffentlichen wäre eine Lüge über ein Produkt, das durchaus alles davon können mag. + +Zwei weitere Dinge, die wir nicht feststellen konnten: irgendein Veröffentlichungsdatum der +aktuellen Version, und ob sie noch aktiv entwickelt wird — der Anbieter wurde im Februar 2025 +übernommen, und die Produktseite trägt ein Copyright von 2016. diff --git a/content/reference/de/codebase-aresmush.md b/content/reference/de/codebase-aresmush.md new file mode 100644 index 0000000..91a72db --- /dev/null +++ b/content/reference/de/codebase-aresmush.md @@ -0,0 +1,34 @@ +--- +kind: codebase +slug: aresmush +title: AresMUSH +summary: Ein moderner Rollenspiel-Server in Ruby, mit Web-Oberfläche und eingebauten Szenen-Werkzeugen statt Softcode. +codebase: AresMUSH +home: https://aresmush.com/ +see-also: collaborative-roleplay +see-also: codebases/pennmush +see-also: codebases/evennia +--- + +AresMUSH ist der neueste weit verbreitete Server, der ausdrücklich auf **gemeinsames Rollenspiel** +zielt, und er bezieht eine andere Position als die TinyMUSH-Linie, deren Nachfolge er antritt. Wo +ein PennMUSH-Spiel sein Szenensystem, seine Charakterbögen und seine Job-Warteschlange aus Softcode +baut, den geschrieben hat, wer gerade da war, liefert Ares all das als fertige Funktionen mit und +erwartet von der Spielleitung, sie zu konfigurieren statt sie zu programmieren. + +Es bringt ein **Web-Portal** mit — Charakter-Wikis, Szenen-Logs, Foren und das Spiel selbst, alles +aus einem Browser erreichbar —, was für ein Genre, in dem die Logs hinterher gelesen werden, ein +Unterschied der Art und nicht bloß des Grades ist. + +Konfiguriert wird in YAML; Erweiterungen sind Ruby-Plugins. Für Spieler gibt es keine +Programmiersprache im Spiel, und das ist der Handel: weniger Strick, weniger Unfälle mit dem Strick +und weniger von jener improvisierenden Baukultur, nach der die MUSH-Linie benannt ist. + +## Wie es von außen aussieht + +Kein MSSP. Es beantwortet ein `WHO` vor der Anmeldung, und die Antwort ist eine **Liste pro +Spieler** statt einer nackten Zahl; unser Parser zählt sie anhand ihrer Struktur. Auf dem Spiel, das +wir gemessen haben, wurden keine Telnet-Optionen ausgehandelt. + +Wenn Sie für ein neues Rollenspiel zwischen diesem und PennMUSH wählen, lautet die Frage ungefähr, +ob Sie ein System wollen, das Sie konfigurieren, oder eines, das Sie schreiben. diff --git a/content/reference/de/codebase-circlemud.md b/content/reference/de/codebase-circlemud.md new file mode 100644 index 0000000..9b6ae85 --- /dev/null +++ b/content/reference/de/codebase-circlemud.md @@ -0,0 +1,27 @@ +--- +kind: codebase +slug: circlemud +title: CircleMUD +summary: Die DikuMUD-Ableitung, die gut genug dokumentiert war, um damit zu unterrichten. Ungeheuer oft geforkt; läuft immer noch. +codebase: CircleMUD +home: https://www.circlemud.org/ +see-also: codebases/dikumud +see-also: codebases/tbamud +see-also: codebases/rom +--- + +CircleMUD ist eine DikuMUD-Ableitung, deren hervorstechendes Merkmal nie eine Spielmechanik war, +sondern die **Dokumentation**. Jeremy Elsons Veröffentlichung war sauber, kommentiert und von einem +Programmierleitfaden begleitet, und so wurde sie die Codebase, an der Leute C lernten, aus der sie +einen MUD betrieben und die sie forkten, ohne vorher irgendetwas rückentwickeln zu müssen. + +Die Folge ist, dass sehr viele laufende Spiele um mehrere Generationen entfernte Circle-Ableitungen +sind, oft ohne dass das Wort irgendwo auftaucht, wo ein Spieler es zu sehen bekäme. + +Die Entwicklung von Circle selbst ist längst abgeschlossen; **tbaMUD** ist die Fortsetzung, und ein +Circle-Spiel, das heute gepflegt wird, wird meist als tbaMUD gepflegt. + +## Wie es von außen aussieht + +MSSP, auf Anfrage beantwortet. Kein `WHO` am Anmeldebildschirm — die Diku-Familie bietet in der +Regel keines an, weshalb ein Verzeichnis, das nur Anmeldebildschirme liest, hier nichts sieht. diff --git a/content/reference/de/codebase-cobramush.md b/content/reference/de/codebase-cobramush.md new file mode 100644 index 0000000..1f937f1 --- /dev/null +++ b/content/reference/de/codebase-cobramush.md @@ -0,0 +1,26 @@ +--- +kind: codebase +slug: cobramush +title: CobraMUSH +summary: Ein PennMUSH-Fork mit eigenem Divisions- und Befugnismodell. Geringe Verbreitung, antwortet weiterhin. +codebase: CobraMUSH +home: https://cobramush.org/ +see-also: codebases/pennmush +see-also: codebases/rhostmush +--- + +CobraMUSH ist ein Fork von PennMUSH und hat ein *Division*-Modell hinzugefügt — eine Hierarchie +administrativer Autorität mit delegierbaren Befugnissen anstelle der flachen Unterscheidung zwischen +wizard und royalty, die das Elternprojekt verwendet. Seine Klientel sind Spiele, die Teile der +Leitungsbefugnis abgeben wollen, ohne alles abzugeben. + +Für PennMUSH geschriebener Softcode läuft größtenteils, und die Unterschiede ballen sich genau in +dem Bereich, um den es beim Fork ging. + +## Wie es von außen aussieht + +Kein MSSP, ein funktionierendes `WHO` vor der Anmeldung und überhaupt keine ausgehandelten +Telnet-Optionen auf dem Spiel, das wir gemessen haben. Der letzte Punkt ist kein Vorwurf: Ein +Server, der nichts aushandelt, ist ein Server, der beim Aushandeln nichts falsch machen kann, und +einfacher Text über einen einfachen Socket ist genau das, womit jeder Client in diesem Hobby +umgeht. diff --git a/content/reference/de/codebase-coffeemud.md b/content/reference/de/codebase-coffeemud.md new file mode 100644 index 0000000..d6cb247 --- /dev/null +++ b/content/reference/de/codebase-coffeemud.md @@ -0,0 +1,29 @@ +--- +kind: codebase +slug: coffeemud +title: CoffeeMUD +summary: Ein MUD-Server in Java, mit dem größten MSSP-Bericht von allem, was wir abgefragt haben, und einer ungewöhnlich breiten Protokollfläche. +codebase: CoffeeMUD +home: https://www.coffeemud.net/ +see-also: codebases/dikumud +see-also: protocols/mssp +--- + +CoffeeMUD ist ein MUD-Server in Java mit ungewöhnlich breitem Funktionsumfang — er bringt einen +eigenen Webserver, Mail, Foren sowie ein großes Klassen- und Fertigkeitensystem mit und ist einer +der wenigen Server im Hobby, die nicht in C geschrieben sind. + +Er wird aktiv gepflegt, was nach den Maßstäben dieses Teils des Katalogs der ausdrücklichen +Erwähnung wert ist. + +## Wie es von außen aussieht + +MSSP und **MCCP2**, und CoffeeMUD ist einer von nur drei Servern unter zwanzig, die wir probiert +haben, der auch die *Klartext*-Form `MSSP-REQUEST` beantwortet hat — eine Variante, die älter ist +als die Telnet-Option und immer noch gelegentlich vorkommt. + +Sein MSSP-Bericht ist der größte, den wir gemessen haben: **47 Felder**, darunter `PORT`, neunmal +getrennt gemeldet für neun getrennte Ports. Das ist keine Fehlbildung. MSSP-Variablen sind Listen, +und ein Crawler, der ein mehrwertiges `PORT` zu einer einzigen Zeichenkette plattdrückt, macht aus +`"80" "23" "4201"` die ganze Zahl `80234201` — ein Fehler, den dieses Projekt ausgeliefert und +behoben hat, und der Grund, warum der Parser hier Werte durchgehend als Listen behält. diff --git a/content/reference/de/codebase-dikumud.md b/content/reference/de/codebase-dikumud.md new file mode 100644 index 0000000..50eb632 --- /dev/null +++ b/content/reference/de/codebase-dikumud.md @@ -0,0 +1,38 @@ +--- +kind: codebase +slug: dikumud +title: DikuMUD +summary: Die Wurzel der Kampf-MUD-Familie. Level, Klassen, Ausrüstung und Area-Dateien — und eine Lizenz, die eine ganze Generation von Ableitungen geprägt hat. +codebase: DikuMUD +home: https://dikumud.com/ +see-also: codebases/circlemud +see-also: codebases/rom +see-also: codebases/smaug +see-also: mush-mud-muck-moo +--- + +DikuMUD, geschrieben am Datalogisk Institut der Universität Kopenhagen und 1991 veröffentlicht, ist +der Vorfahr des meisten von dem, was gemeint ist, wenn jemand ohne nähere Bestimmung „MUD“ sagt. +Level, Charakterklassen, Trefferpunkte, Mobs, Ausrüstungsplätze, ein Area-Dateiformat, das ein +Builder offline schreibt — das ganze Vokabular kommt von hier, und Spiele, die nie Diku-Quelltext +gesehen haben, erben trotzdem seine Form. + +Seine Lizenz gehört zur Geschichte. Diku war frei nutzbar, verbot aber, Geld für den Zugang zu +verlangen, und verlangte, die ursprünglichen Credits anzuzeigen; diese Klausel ist der Grund, warum +„die Diku-Credits“ auf dem Anmeldebildschirm von Spielen stehen, die mehrere Forks von ihm entfernt +sind. + +Die direkten Nachfahren — **Merc**, dann **ROM**, **CircleMUD**, **SMAUG**, **tbaMUD** und Dutzende +weitere — machen einen großen Teil jedes MUD-Verzeichnisses aus, das je existiert hat. + +## Wie es von außen aussieht + +Die Diku-Familie ist die **MSSP**-Familie. Während die MUSH-Seite eine Zählung über ein `WHO` am +Anmeldebildschirm veröffentlicht und überhaupt kein MSSP anbietet, beantworten Server der Diku-Linie +ganz überwiegend die Telnet-Option 70 mit einem strukturierten Bericht, und daher stammen ihre +Zahlen hier. + +**MCCP2** — Stromkompression — ist in dieser Familie ebenfalls verbreitet, und es lohnt sich zu +wissen, dass ein Client, der sie aushandelt, den Strom aber nicht entpacken kann, den gesamten +Verbindungsbildschirm als binäres Rauschen erhält. Das war ein echter Defekt in der eigenen +Telnet-Bibliothek dieses Projekts und ist behoben; siehe [MCCP](/reference/protocols/mccp). diff --git a/content/reference/de/codebase-evennia.md b/content/reference/de/codebase-evennia.md new file mode 100644 index 0000000..8fdc559 --- /dev/null +++ b/content/reference/de/codebase-evennia.md @@ -0,0 +1,34 @@ +--- +kind: codebase +slug: evennia +title: Evennia +summary: Eher ein Python-Framework als ein fertiges Spiel. Zwei Evennia-Spiele können außer dem Unterbau nichts gemeinsam haben. +codebase: Evennia +home: https://www.evennia.com/ +see-also: codebases/aresmush +see-also: collaborative-roleplay +see-also: protocols/gmcp +--- + +Evennia ist ein **MU\*-Framework**, kein Spiel — das ist das Erste, was man darüber wissen muss, und +das ist es, was den Vergleich von Evennia-Spielen untereinander wenig hilfreich macht. Es ist eine +Python-Bibliothek auf Basis von Django und Twisted, die Ihnen Accounts, Objekte, Räume, Befehle, +eine Persistenzschicht und den Netzwerk-Stack gibt und dann erwartet, dass Sie das Spiel schreiben. + +Die Folge ist, dass „läuft auf Evennia“ weit weniger über ein Spiel aussagt als „läuft auf +PennMUSH“. Es gibt Kampf-MUDs auf Evennia und es gibt Rollenspiele auf Evennia, und sie teilen kein +Vokabular. Zwei Evennia-Spiele haben womöglich keinen einzigen Befehl gemeinsam. + +Wer Python bereits kennt, hat hier den kürzesten Weg von nichts zu einer laufenden Welt, und dort +hat ein guter Teil der neuen Spiele seit Mitte der 2010er begonnen. + +## Wie es von außen aussieht + +Evennia bietet **MSSP** an und veröffentlicht darüber eine Spielerzahl. Auf dem Spiel, das wir +gemessen haben, hat es außerdem **MCCP2** ausgehandelt — Kompression —, was für einen Stack +charakteristisch ist, der sein Telnet ernst genommen hat. + +Weil Evennia ein Framework ist, ist das, was ein bestimmtes Spiel aushandelt, zum Teil die +Entscheidung des Spiels. Die Verbreitungszahlen auf den Protokollseiten zählen, was Server uns +tatsächlich angeboten haben, nicht, was das Framework kann, und bei Evennia liegen diese beiden +weiter auseinander als bei den meisten. diff --git a/content/reference/de/codebase-fluffos.md b/content/reference/de/codebase-fluffos.md new file mode 100644 index 0000000..b3ab6ce --- /dev/null +++ b/content/reference/de/codebase-fluffos.md @@ -0,0 +1,34 @@ +--- +kind: codebase +slug: fluffos +title: FluffOS +summary: Der gepflegte MudOS-Nachfolger und der Driver, auf dem die meisten überlebenden LPMud-Spiele laufen. Das Spiel ist in LPC geschrieben, nicht in C. +codebase: FluffOS +home: https://www.fluffos.info/ +see-also: codebases/dikumud +see-also: mush-mud-muck-moo +--- + +Die LPMud-Tradition teilt die Welt anders auf als Diku. Es gibt einen **Driver** — ein C-Programm, +das einen objektorientierten Interpreter ausführt — und eine **Mudlib**, die das gesamte Spiel ist, +in **LPC** geschrieben und vom Driver geladen. Räume, Kampf, Befehle und der Anmeldeablauf sind +allesamt Mudlib-Objekte; der Driver weiß von keinem davon. + +Das rückt ein LPMud dem Geist nach näher an ein MUSH heran, als seine Kampfsysteme vermuten lassen: +Das Spiel ist in einer Sprache geschrieben, die im Spiel selbst lebt, und zwei LPMuds mit demselben +Driver teilen möglicherweise sonst nichts. + +**MudOS** war jahrelang der vorherrschende Driver; **FluffOS** ist seine gepflegte Fortsetzung und +das, worauf ein laufendes LP-Spiel heute am ehesten läuft. Bekannte Mudlibs — Nightmare, Lima, +Discworlds eigene — sind wiederum eigene Projekte. + +## Wie es von außen aussieht + +MSSP und **MCCP2** auf dem FluffOS-Spiel, das wir gemessen haben. MudOS war eine von nur zwei +Codebases in unserer Erhebung, die *sowohl* MSSP als auch ein `WHO` am Anmeldebildschirm beantwortet +haben, wobei das `WHO` allerdings eine Auflistung pro Spieler statt einer Zählung lieferte. + +Weil die Mudlib das Spiel ist, ist das, was ein bestimmtes LP-Spiel aushandelt, ebenso sehr eine +Entscheidung der Mudlib wie eine des Drivers — die Verbreitungszahlen auf den Protokollseiten +zählen, was Server uns tatsächlich angeboten haben, was für diese Familie ein schwächeres Signal +über die Codebase ist als anderswo. diff --git a/content/reference/de/codebase-moo.md b/content/reference/de/codebase-moo.md new file mode 100644 index 0000000..d06f387 --- /dev/null +++ b/content/reference/de/codebase-moo.md @@ -0,0 +1,30 @@ +--- +kind: codebase +slug: moo +title: MOO +summary: Objektorientiert, vollständig von innen heraus bearbeitet und ebenso sehr Forschungs- und Lehrplattform wie Spiel-Engine. +codebase: MOO +home: https://www.ipomoea.org/moo/ +see-also: mush-mud-muck-moo +see-also: codebases/muck +--- + +MOO — *MUD, Object-Oriented* — treibt die Idee, dass die Welt sich selbst bearbeitet, weiter als +alles andere in diesem Hobby. LambdaMOO, der ursprüngliche Server, liefert einen kleinen C-Kern und +eine Datenbank; im Grunde alles, was man dort erlebt, ist **in der MOO-Sprache, innerhalb der +laufenden Datenbank, von den Leuten geschrieben, die sie benutzen**. Für einen Raum gibt es keine +Quelldatei. + +Diese Eigenschaft gab MOOs ein Leben außerhalb von Spielen. In den Neunzigern wurden sie für Lehre, +Konferenzen und Forschung eingesetzt — Diversity University, BioMOO, Jay's House —, und die +technische Literatur über MOO ist für eine Codebase in diesem Feld überproportional akademisch. + +Die Verbreitung ist heute klein, aber wirklich nicht null, und die verbliebenen Server laufen oft +seit Jahrzehnten ununterbrochen. + +## Wie es von außen aussieht + +Kein MSSP, und auf dem Spiel, das wir gemessen haben, kein `WHO`, das wir auswerten konnten. Was es +hatte, war ein Satz in seinem Verbindungsbildschirm: *„one of three players are active“* — daher +stammt der Leser für ausgeschriebene Zahlen in diesem Crawler. Ein Parser, der nur Ziffern kennt, +sieht dort überhaupt keine Zählung und hätte dieses Spiel für immer als unbekannt gemeldet. diff --git a/content/reference/de/codebase-muck.md b/content/reference/de/codebase-muck.md new file mode 100644 index 0000000..37c9560 --- /dev/null +++ b/content/reference/de/codebase-muck.md @@ -0,0 +1,31 @@ +--- +kind: codebase +slug: muck +title: MUCK +summary: Ein TinyMUD-Nachfahre mit einer eigenen Forth-artigen Sprache im Spiel und einer sozialen Kultur, die sich von der MUSH-Seite unterscheidet. +codebase: MUCK +home: https://www.fuzzball.org/ +see-also: mush-mud-muck-moo +see-also: codebases/tinymush +see-also: codebases/moo +--- + +MUCK — in der Praxis fast immer **Fuzzball MUCK** — ist eher ein Geschwister der MUSH-Linie als ihr +Nachfahre: Beide stammen von TinyMUD ab, und beide setzen eine Programmiersprache ins Spiel hinein. + +Die Sprache ist der sichtbare Unterschied. MUF (*Multi-User Forth*) ist stapelbasiert und liest sich +überhaupt nicht wie MUSH-Softcode; wer in der einen geübt ist, ist in der anderen Anfänger. Darüber +sitzt MPI, eine kleinere, eingebettete Ausdruckssprache für das, was auf einem MUSH der Softcode +täte. + +Kulturell ist MUCK die Heimat eines großen Teils der geselligen und der Fandom-Welten dieses Hobbys. +Diese Spiele sind eher um Anwesenheit und Gespräch herum gebaut als um Szenen mit einem Anfang und +einem Ende, was ein echter Unterschied zur Rollenspiel-MUSH-Tradition ist und keine Frage des +Themas. + +## Wie es von außen aussieht + +Kein MSSP. Ein `WHO` vor der Anmeldung, das mit einer Zählung antwortet. Auf dem Spiel, das wir +gemessen haben, wurden keine Telnet-Optionen ausgehandelt — und eine Einzelheit aus der Erhebung ist +festzuhalten: Seine `WHO`-Antwort endete auf ein Leerzeichen ohne Zeilenumbruch, und genau so etwas +bringt einen naiven Parser dazu, gar nichts zu melden. diff --git a/content/reference/de/codebase-pennmush.md b/content/reference/de/codebase-pennmush.md new file mode 100644 index 0000000..ea4db81 --- /dev/null +++ b/content/reference/de/codebase-pennmush.md @@ -0,0 +1,52 @@ +--- +kind: codebase +slug: pennmush +title: PennMUSH +summary: Der am weitesten verbreitete MUSH-Server. Softcode, eine lange Veröffentlichungsgeschichte und eine von nur zwei Codebases in unserer Erhebung, die sowohl MSSP als auch ein WHO vor der Anmeldung beantworten. +codebase: PennMUSH +home: https://www.pennmush.org/ +see-also: codebases/tinymux +see-also: codebases/rhostmush +see-also: codebases/cobramush +see-also: mush-mud-muck-moo +see-also: protocols/mssp +--- + +PennMUSH stammt über einen Fork von 1991 von TinyMUSH ab, und es ist der Server, auf dem die meisten +langlebigen Rollenspiel-MUSHes laufen. Sein bestimmendes Merkmal ist **Softcode**: eine funktionale +Ausdruckssprache, von innerhalb des Spiels bearbeitet von jedem, der das passende Bit gesetzt hat, +und in ihr ist ein großer Teil des Verhaltens jedes einzelnen MUSH geschrieben. Ein PennMUSH-Spiel +wird weniger konfiguriert als von seinen Spielern programmiert. + +Versionen lesen sich als `1.8.8p0` — eine Hauptversion, eine Nebenversion und ein Patchlevel —, und +der Patchlevel bewegt sich oft. Spiele laufen häufig mit einer Version, die mehrere Patchlevel +zurückliegt, was nicht weiter bemerkenswert ist. + +## Wie es von außen aussieht + +PennMUSH ist eine von nur zwei Codebases in unserer eigenen Erhebung über 38 Server, die *beide* +Wege beantwortet hat, die wir abfragen. Es bietet MSSP an, wenn man fragt, und es beantwortet ein +`WHO`, das am Anmeldebildschirm getippt wird, und auf dem Spiel, das wir gemessen haben, stimmten +die beiden überein — was seltener ist, als es klingt, und PennMUSH zu der Kontrolle machte, an der +wir andere Server geprüft haben. + +Das `WHO` vor der Anmeldung ist mehr als eine Bequemlichkeit: Es ist überhaupt die Art, wie die +MUSH-Familie eine Spielerzahl veröffentlicht, denn der größte Teil der übrigen Familie bietet gar +kein MSSP an. Unter [MSSP](/reference/protocols/mssp) steht, warum diese Spaltung der Grund dafür +ist, dass diese Website vier Schichten abfragt statt einer. + +CHARSET-Aushandlung ist auf modernem PennMUSH normal, weshalb Namen mit Akzenten die Reise +überstehen. + +## Verwandte Server + +PennMUSH, **TinyMUX**, **RhostMUSH** und **CobraMUSH** sind vier Server mit gemeinsamem Vorfahren +und gemeinsamem Vokabular — wer einen kennt, kann den Softcode eines anderen mit Mühe lesen. +Kompatibel sind sie nicht: Eine Datenbank wandert nicht ohne Konvertierung von einem zum anderen, +und die Funktionsbibliotheken unterscheiden sich auf Weisen, die zählen. + +## SharpMUSH + +Eine .NET-Neuimplementierung mit dem Ziel der PennMUSH-Kompatibilität ist in Entwicklung, von +demselben Autor wie diese Website. Nichts auf dieser Seite ist an ihr gemessen, und sie hat keine +Spiele im Katalog. diff --git a/content/reference/de/codebase-rhostmush.md b/content/reference/de/codebase-rhostmush.md new file mode 100644 index 0000000..220c344 --- /dev/null +++ b/content/reference/de/codebase-rhostmush.md @@ -0,0 +1,28 @@ +--- +kind: codebase +slug: rhostmush +title: RhostMUSH +summary: Ein MUSH-Server, bekannt für ein tiefes Rechtemodell und einen großen Satz eingebauter Funktionen. Kein MSSP; beantwortet ein WHO vor der Anmeldung. +codebase: RhostMUSH +home: https://github.com/RhostMUSH/trunk +see-also: codebases/pennmush +see-also: codebases/tinymux +see-also: codebases/cobramush +--- + +RhostMUSH ist der vierte der weit verbreiteten, von TinyMUSH abstammenden Server und der mit dem +ausgefeiltesten Verwaltungsmodell: Sein Rechte- und Flag-System ist erheblich feinkörniger als das +seiner Verwandten, und das ist der übliche Grund, aus dem ein Spiel sich für ihn entscheidet. + +Seine Bibliothek eingebauter Funktionen ist groß, und für Rhost geschriebener Softcode lässt sich +oft nicht sauber nach PennMUSH oder TinyMUX übertragen, ohne die Stellen umzuschreiben, die +Funktionen benutzt haben, die es dort nicht gibt. + +## Wie es von außen aussieht + +Kein MSSP. Ein `WHO` vor der Anmeldung, das mit einer Zählung antwortet. CHARSET wird ausgehandelt. + +Diese Kombination — kein MSSP, ein funktionierendes `WHO` — ist die Signatur der MUSH-Familie, und +sie ist der Grund, warum diese Website den Anmeldebildschirm überhaupt abfragt. Nach dem Befund +unserer eigenen Erhebung sind die MSSP- und die `WHO`-Familie nahezu disjunkt: 28 Codebases +veröffentlichen eine Zählung über MSSP, sieben über `WHO` und nur zwei über beides. diff --git a/content/reference/de/codebase-rom.md b/content/reference/de/codebase-rom.md new file mode 100644 index 0000000..f82966f --- /dev/null +++ b/content/reference/de/codebase-rom.md @@ -0,0 +1,32 @@ +--- +kind: codebase +slug: rom +title: ROM +summary: Der bekannteste Nachfahre von Merc und die Kampf-Engine, auf der ein großer Teil der Neunziger-MUDs aufgebaut wurde. +codebase: ROM +see-also: codebases/dikumud +see-also: codebases/smaug +see-also: protocols/mccp +--- + +ROM — *Rivers of MUD* — ist eine Ableitung von **Merc**, das selbst eine DikuMUD-Ableitung ist, und +es ist diejenige, die sich durchgesetzt hat. Sein Kampfmodell, sein Fertigkeits- und Zaubersystem +und sein Area-Format waren der Ausgangspunkt für eine enorme Zahl von Spielen in den Neunzigern und +danach, und besonders ROM 2.4 ist einer der meistgeforkten Quelltexte im Hobby. + +Wie der Rest der Diku-Linie trägt es die ursprüngliche Credits-Auflage mit sich, und so nennt ein +Spiel, dessen Abstammung sich sonst nicht ermitteln lässt, auf seinem Anmeldebildschirm oft Diku, +Merc und ROM. + +## Wie es von außen aussieht + +MSSP, CHARSET und **MCCP2**, auf dem Spiel, das wir gemessen haben. + +ROM ist der Server, an dem dieses Projekt seinen eigenen Kompressionsfehler nachgewiesen hat. Unsere +Abfrage handelte MCCP2 aus, der Server begann korrekt zu komprimieren, und die Telnet-Bibliothek, +auf die wir angewiesen sind, hat den Strom nie entpackt — der Verbindungsbildschirm kam also als +Wand aus Ersatzzeichen an, und wir haben das kurzzeitig als Schuld des Spiels verbucht. Die Nutzlast +ließ sich mit einem gewöhnlichen zlib-Aufruf sauber dekomprimieren, und das machte die Sache +eindeutig. Es wurde upstream behoben; die Geschichte steht auf der Seite +[MCCP](/reference/protocols/mccp), weil sie ein gutes Beispiel für einen Defekt ist, der von außen +genau wie ein kaputtes Spiel aussieht. diff --git a/content/reference/de/codebase-smaug.md b/content/reference/de/codebase-smaug.md new file mode 100644 index 0000000..11022c0 --- /dev/null +++ b/content/reference/de/codebase-smaug.md @@ -0,0 +1,28 @@ +--- +kind: codebase +slug: smaug +title: SMAUG +summary: Eine Merc-Ableitung für große thematische Welten, mit Gottheiten, Ebenen und einem Area-Bestand um ein Mehrfaches größer als der ihres Elternprojekts. +codebase: SMAUG +home: https://www.smaug.org/ +see-also: codebases/dikumud +see-also: codebases/rom +--- + +SMAUG — *Simulated Medieval Adventure multi-User Game* — kam aus dem Realms of Despair und ist eine +Merc-Ableitung, die gewachsen ist statt vereinfacht. Wo ROM Mercs Kampf gestrafft hat, hat SMAUG +hinzugefügt: Gottheiten, Ebenen, ausgefeiltere Area- und Mob-Programme, eine viel größere +Standardwelt und einen entsprechend größeren Quellbaum. + +Die Spiele, die es einsetzen, sind eher die großen thematischen, und mehrere laufen seit +fünfundzwanzig Jahren ununterbrochen. + +## Wie es von außen aussieht + +MSSP, auf Anfrage beantwortet, und sonst nichts ausgehandelt auf dem Spiel, das wir gemessen haben. + +Hinweis für alle, die Zählungen vergleichen: SMAUG-Server in unserer Erhebung haben über MSSP +berichtet und kein `WHO` am Anmeldebildschirm beantwortet, die Zahl hier ist also das eigene MSSP +`PLAYERS` des Spiels — eine Behauptung des Servers, die nur deshalb für bare Münze genommen wird, +weil wir sie nicht unabhängig messen konnten. Die Spielseite kennzeichnet, aus welcher der beiden +Quellen eine gegebene Zahl stammt. diff --git a/content/reference/de/codebase-tbamud.md b/content/reference/de/codebase-tbamud.md new file mode 100644 index 0000000..3f09904 --- /dev/null +++ b/content/reference/de/codebase-tbamud.md @@ -0,0 +1,26 @@ +--- +kind: codebase +slug: tbamud +title: tbaMUD +summary: Die gepflegte Fortsetzung von CircleMUD und die übliche Antwort auf „Ich will heute einen MUD im Diku-Stil aufmachen“. +codebase: tbaMUD +home: https://tbamud.com/ +see-also: codebases/circlemud +see-also: codebases/dikumud +--- + +tbaMUD — *The Builder Academy MUD* — hat CircleMUD dort aufgenommen, wo dessen eigene Entwicklung +aufhörte, und baut, patcht und dokumentiert es seither weiter. Wenn jemand einen MUD mit Leveln und +Kampf von einer bekanntermaßen guten Grundlage aus beginnt statt vom eigenen Fork, dann ist das +diese Grundlage. + +Es trägt die Tugenden der Circle-Codebase weiter: lesbares C, ein dokumentiertes Area-Dateiformat +und ein Builder-Handbuch, das voraussetzt, dass Sie das noch nie gemacht haben. Die Builder Academy +selbst läuft als Lehrspiel, in dem Leute das Bauen darauf lernen. + +## Wie es von außen aussieht + +MSSP, auf Anfrage beantwortet. Eine gemessene Null auf dem Spiel, das wir abgefragt haben — was auf +dieser Website eine gefüllte Zelle ist und keine leere: Wir sind hineingekommen, wir haben die Zahl +abgelesen, und die Zahl war null. Das ist eine andere Tatsache als ein Spiel, das wir nicht zählen +konnten, und die beiden werden hier nie zusammengeführt. diff --git a/content/reference/de/codebase-tinymush.md b/content/reference/de/codebase-tinymush.md new file mode 100644 index 0000000..01e296b --- /dev/null +++ b/content/reference/de/codebase-tinymush.md @@ -0,0 +1,41 @@ +--- +kind: codebase +slug: tinymush +title: TinyMUSH +summary: Der Vorfahr der MUSH-Linie, auf dem noch Spiele laufen. Er hat diesem Crawler beigebracht, dass dessen eigene Aushandlungsbytes den nächsten Befehl kaputtmachen können, den er sendet. +codebase: TinyMUSH +home: https://github.com/TinyMUSH/TinyMUSH +see-also: codebases/pennmush +see-also: codebases/tinymux +see-also: mush-mud-muck-moo +--- + +TinyMUSH ist das, wovon die Linie PennMUSH, TinyMUX, RhostMUSH und CobraMUSH allesamt abstammt, und +es ist immer noch im Einsatz. Die Entwicklung ist eher still als abwesend. + +## Wie es von außen aussieht + +Kein MSSP. Ein `WHO` vor der Anmeldung, das mit einem Satz der Form `0 Players logged in, 22 +record, no maximum.` antwortet. + +## Der Fehler, den es bei uns gefunden hat + +TinyMUSH ist hier einen Absatz wert, weil es das Spiel ist, das einen Defekt im eigenen Crawler +dieser Website aufgedeckt hat, und die Korrektur ist eine gute Veranschaulichung dessen, was +„gemessen“ heißen soll. + +Unsere Abfrage las TinyMUSH wochenlang als *Zählung unbekannt*. Die Vermutung zu den Akten war, dass +seine Antwort keinen abschließenden Zeilenumbruch habe. Sie hat einen. Vom Draht mitgeschnitten, war +die wirkliche Ursache unsere: **TinyMUSH wertet an seinem Anmeldebildschirm kein Telnet aus**, also +landen die drei Bytes `IAC DO MSSP`, die wir beim Verbinden senden, in seinem Eingabepuffer, als +hätte jemand sie getippt. Die nächste Zeile, die es liest, ist nicht `WHO`, sondern drei +Steuerbytes gefolgt von `WHO`, und das ist kein Befehl, den es hat — also zeigt es seinen +Verbindungsbildschirm erneut an und sagt nichts über Spieler. + +Die Abfrage sendet nach dem Aushandeln nun einen leeren Zeilenumbruch und verwirft, was immer daraus +folgt, denn diese Ausgabe ist eine Reaktion auf Bytes, die *wir* zu senden gewählt haben, und ist +daher weder der Verbindungsbildschirm des Spiels noch seine Antwort. TinyMUSH wird jetzt korrekt +gelesen, und die Abfrage war in einem Drittel der Zeit fertig. + +Ein Verzeichnis, das nicht nachgeprüft hätte, hätte „dieses Spiel meldet seine Spieler nicht“ +veröffentlicht, solange es besteht, und der Satz wäre über uns gewesen. diff --git a/content/reference/de/codebase-tinymux.md b/content/reference/de/codebase-tinymux.md new file mode 100644 index 0000000..e392ebc --- /dev/null +++ b/content/reference/de/codebase-tinymux.md @@ -0,0 +1,38 @@ +--- +kind: codebase +slug: tinymux +title: TinyMUX +summary: Der andere große MUSH-Server. Softcode nah genug an dem von PennMUSH, um darüber zu streiten, gar kein MSSP und ein funktionierendes WHO vor der Anmeldung. +codebase: TinyMUX +home: https://www.tinymux.org/ +see-also: codebases/pennmush +see-also: codebases/tinymush +see-also: codebases/rhostmush +see-also: mush-mud-muck-moo +--- + +TinyMUX ist der zweite der beiden Server, auf denen die meisten etablierten Rollenspiel-MUSHes +laufen, und für viele Spieler ist die Wahl zwischen ihm und PennMUSH eine Frage dessen, welchen die +Spielleitung ihres Spiels zuerst gelernt hat. Versionen lesen sich als `2.12` und ähnlich. + +Wie PennMUSH stammt es von TinyMUSH ab, und sein Softcode ist nah genug, dass jemand, der zwischen +beiden wechselt, übersetzt statt neu zu lernen. Die Unterschiede sind real — Funktionsbibliotheken, +einige Ecken des Parsens, der Satz der `@`-Befehle — und genau die Art von Sache, die das +Verschieben einer Datenbank zwischen beiden zu einem Projekt macht statt zu einem Export. + +## Wie es von außen aussieht + +**Kein MSSP.** TinyMUX bietet die Option überhaupt nicht an, was es zusammen mit AresMUSH, MUCK, +RhostMUSH, CobraMUSH und TinyMUSH auf jene Seite des Hobbys stellt, die ein Verzeichnis auf reiner +MSSP-Basis schlicht nicht sehen kann. Seine Spielerzahl kommt von einem `WHO` am Anmeldebildschirm, +das es mit einer schlichten Zählung beantwortet. + +CHARSET handelt es hingegen aus, und damit kommt es bei nicht-ASCII-Text besser weg als die meisten +seiner Verwandten. + +## Woher die Zählungen stammen + +Wenn Sie die Zahl dieser Website für ein TinyMUX-Spiel mit der eines anderen Verzeichnisses +vergleichen, beachten Sie: Wir lesen das `WHO` am Anmeldebildschirm, und die meisten Crawler tun das +nicht. Ein Verzeichnis, das allein auf MSSP baut, meldet diese Spiele als solche ganz ohne Zählung — +oder führt sie nicht auf. diff --git a/content/reference/de/orientation-collaborative-roleplay.md b/content/reference/de/orientation-collaborative-roleplay.md new file mode 100644 index 0000000..a6bce4c --- /dev/null +++ b/content/reference/de/orientation-collaborative-roleplay.md @@ -0,0 +1,67 @@ +--- +kind: orientation +slug: collaborative-roleplay +title: Sie wollen gemeinsames Rollenspiel +summary: Wenn es Ihnen darum geht, mit anderen Menschen Szenen zu schreiben, ist das hier der kurze Weg — nach welchen Codebases Sie suchen, ein Client, der Ihnen nicht im Weg steht, und was Sie am ersten Abend erwartet. +see-also: mush-mud-muck-moo +see-also: connecting +see-also: codebases/pennmush +see-also: codebases/aresmush +see-also: codebases/evennia +see-also: clients/mudlet +--- + +Mit *gemeinsamem Rollenspiel* meinen wir: Sie und andere Menschen schreiben zusammen eine Szene, in +der Rolle, in Prosa, meist in Echtzeit. Niemand würfelt Initiative, außer das Spiel verlangt es. Das +ist der Teil des Hobbys, den das Wort „MUD“ verdeckt, und er lebt. + +## Halten Sie nach diesen Codebases Ausschau + +Diese Kultur lebt in der TinyMUD-Linie, denn ihre Server sind für Leute gebaut, die Dinge erschaffen, +und nicht für Leute, die Dinge töten. + +- **[PennMUSH](/reference/codebases/pennmush)** und **[TinyMUX](/reference/codebases/tinymux)** — + die zwei großen MUSH-Server. Die meisten langlebigen Rollenspiele, von denen Sie gehört haben, + laufen auf einem von beiden. +- **[AresMUSH](/reference/codebases/aresmush)** — modern, eigens für Rollenspiel gebaut, mit einem + Web-Frontend und Szenenprotokollen als Funktionen erster Klasse statt als Softcode, den jemand + geschrieben hat. +- **[Evennia](/reference/codebases/evennia)** — ein Python-Framework und kein fertiges Spiel. Die + darauf gebauten Spiele unterscheiden sich enorm; mehrere der neueren Rollenspiele sind Evennia. +- **[MUCK](/reference/codebases/muck)** — eine eigene Kultur, stark sozial geprägt, und einen Blick + wert, wenn Sie die Fandom-Welten suchen. + +Nach jedem davon können Sie den Katalog filtern: Jede Codebase-Seite verlinkt die Spiele, bei denen +wir sie gemessen haben. + +## Ein Client, der Ihnen nicht im Weg steht + +Sie brauchen keinen — jedes Spiel hier spricht einfaches Telnet, und Ihr Betriebssystem hat +wahrscheinlich schon einen Client. Aber fürs Szenenschreiben zählen drei Dinge mehr als sonst: + +1. **Eine Befehlszeile, die einen langen Absatz übersteht.** Sie werden Posen über mehrere Sätze + tippen. +2. **Protokollieren.** Sie werden die Szene hinterher haben wollen. +3. **UTF-8.** In Namen stecken Akzente. + +Siehe den Abschnitt [Clients](/reference). Wenn Sie einen Screenreader benutzen: Die +Fähigkeitstabellen dort führen eine Zeile dafür — nicht für jeden Client konnten wir eine Antwort +feststellen, und wo wir es nicht konnten, steht in der Tabelle *unbekannt* statt einer Vermutung. + +## Wie der erste Abend wirklich abläuft + +Die meisten Rollenspiele haben ein **Bewerbungsverfahren**: Sie verbinden sich als Gast, lesen ein +paar Hilfedateien und schreiben einen Charakter auf, den die Spielleitung liest. Das kann einen Tag +dauern oder zwei Wochen. Es ist keine Abschottung um ihrer selbst willen — so hält ein Spiel eine +gemeinsame Welt stimmig —, aber es heißt eben, dass „einloggen und losspielen“ selten die Form der +Sache ist. + +Spiele, auf denen gerade niemand ist, wenn Sie nachsehen, sind hier normal, und das Aktivitätsraster +auf der Seite jedes Spiels ist nützlicher als seine Momentanzahl: Ein Spiel mit fünfzehn Leuten an +jedem Abend und niemandem um 4 Uhr morgens ist ein gesundes Spiel, zur falschen Stunde betrachtet. + +## Was diese Website Ihnen nicht sagen kann + +Ob die Leute etwas taugen. Ob die Spielleitung fair ist. Ob die Welt eine ist, die Ihnen gefallen +würde. Wir messen Server. Es gibt hier keine Bewertung und wird keine geben — das ist eine bewusste +Entscheidung und keine Lücke. diff --git a/content/reference/de/orientation-connecting.md b/content/reference/de/orientation-connecting.md new file mode 100644 index 0000000..24c0ad0 --- /dev/null +++ b/content/reference/de/orientation-connecting.md @@ -0,0 +1,49 @@ +--- +kind: orientation +slug: connecting +title: Wie man sich verbindet +summary: Ein Host, ein Port und Telnet. Was die Adresse auf einer Spielseite bedeutet und was man mit ihr macht. +see-also: mush-mud-muck-moo +see-also: protocols/tls +see-also: protocols/charset +--- + +Jedes hier verzeichnete Spiel antwortet auf einem **Host und einem Port**, und das Protokoll darunter +ist Telnet — was in der Praxis eine rohe TCP-Verbindung mit ein wenig optionaler Aushandlung +obendrauf bedeutet. + + telnet mush.pennmush.org 4201 + +Das funktioniert, und auf vielen Systemen ist es schon installiert. Es ist auch eine schlechte Art zu +spielen: Das `telnet` des Systems hat keine nennenswerte Steuerung des lokalen Echos, kein +Protokoll, keine Historie, und es verstümmelt alles oberhalb von ASCII. Es ist das richtige Werkzeug, +um zu prüfen, ob ein Spiel antwortet, und das falsche, um einen Abend darin zu verbringen. + +## Was Ihnen die Adresse auf einer Spielseite sagt + +Jede Spielseite führt die Endpunkte auf, die wir gemessen haben, und kennzeichnet jeden, bei dem +**TLS** beobachtet wurde. Ein Spiel mit einem TLS-Port ist ein Spiel, zu dem Sie verschlüsselt +verbinden können; die Portnummer ist meist eine andere als die des einfachen Ports. + +Wo ein Spiel mehrere Ports hat, sind das häufig verschiedene Wege zu derselben Welt und nicht +verschiedene Spiele. Wir führen auf, was wir gemessen haben, und raten nicht, welcher der maßgebliche +ist. + +## Einen Client wählen + +Der Abschnitt [Clients](/reference) hat für jeden eine Seite, mit einer Fähigkeitstabelle. Die drei +Dinge, die zu prüfen sich lohnt, bevor Sie irgendetwas installieren: + +- **Kann er UTF-8?** Wenn das Spiel nicht nur englisch ist, kommt das schon am ersten Abend auf. +- **Kann er TLS?** Zählt nur, wenn das Spiel es anbietet, aber mehrere tun das inzwischen. +- **Wenn Sie einen Screenreader benutzen: Dokumentiert das Projekt Unterstützung dafür?** Das ist die + Zeile, die in Client-Vergleichen am häufigsten fehlt, deshalb ist sie bei uns die erste — und wo + niemand eine Antwort festgestellt hat, steht dort *unbekannt*. + +## Wenn nichts antwortet + +Ein Spiel, das nicht antwortet, ist nicht zwangsläufig verschwunden. Spiele ziehen auf andere Hosts +um, DNS-Einträge laufen aus, und Firewalls haben ihre Meinungen. Diese Website behält jedes Spiel, +das sie je gemessen hat — auch die, die vor Jahren aufgehört haben zu antworten —, und klopft weiter +wöchentlich an; das [Archiv](/archive) ist also der Ort, an dem man nachsieht, bevor man irgendetwas +schlussfolgert. diff --git a/content/reference/de/orientation-families.md b/content/reference/de/orientation-families.md new file mode 100644 index 0000000..87833ae --- /dev/null +++ b/content/reference/de/orientation-families.md @@ -0,0 +1,85 @@ +--- +kind: orientation +slug: mush-mud-muck-moo +title: MUSH, MUD, MUCK, MOO — was die Wörter bedeuten +summary: Vier Wörter für vier Traditionen, von denen keine ein Genre ist. Was sie Ihnen tatsächlich sagen. +see-also: collaborative-roleplay +see-also: connecting +see-also: codebases/pennmush +see-also: codebases/aresmush +see-also: codebases/muck +see-also: codebases/moo +see-also: codebases/evennia +--- + +Jedes dieser Wörter benennt eine **Familie von Serversoftware**, keine Art von Spiel. Das ist das mit +Abstand Nützlichste, was man über sie wissen kann, und deshalb wird „Ist das ein MUSH oder ein MUD?“ +so oft schlecht beantwortet: Die ehrliche Antwort lautet meist *beides, und die Frage, die Sie +meinten, ging um die Kultur*. + +## MUD + +Der älteste Begriff und heute der weiteste. Er begann als *Multi-User Dungeon* — Bartles und +Trubshaws Spiel von 1978 — und war Mitte der Neunziger das Sammelwort für jede textbasierte +Mehrspielerwelt. + +Eng gebraucht meint er die **DikuMUD- und LPMud-Linien**: Server, die um Stufen, Kampf, Ausrüstung +und eine Gebietsdatei herum gebaut sind, in der ein Erbauer die Räume im Voraus beschrieben hat. Wenn +jemand sagt „Ich spiele ein MUD“ und etwas Bestimmtes meint, ist es meist das. + +Das Verzeichnis kann Ihnen jede Linie für sich zeigen: [die DikuMUD-Spiele](/games?lineage=DikuMUD) +und [die LPMud-Spiele](/games?lineage=LPMud). + +## MUSH + +*Multi-User Shared Hallucination*, aus der TinyMUD-Linie. Die bestimmende Eigenschaft ist nicht das +Thema, sondern der **Softcode**: MUSH-Server bringen eine Programmiersprache mit, die Spieler von +innerhalb des Spiels benutzen; ein Spieler mit Baurechten legt damit Räume, Objekte und Verhalten an, +ohne eine Quelldatei anzufassen oder irgendetwas neu zu starten. + +Diese eine Entwurfsentscheidung hat die Kultur hervorgebracht. MUSHes haben tendenziell wenig +automatisierte Systeme und viele menschliche — von der Spielleitung geführte Handlungsbögen, +geschriebene Szenen, Bewerbungsverfahren —, weil die Leute, die spielen, auch die Leute sind, die +bauen. + +PennMUSH, TinyMUSH, TinyMUX, RhostMUSH, CobraMUSH und AresMUSH stehen alle in dieser Linie, und +keines von ihnen sagt das: MSSP kennt keinen Wert `MUSH`, den man veröffentlichen könnte, und außer +PennMUSH veröffentlicht keines überhaupt MSSP. Sie zu gruppieren ist daher etwas, das wir tun, und +nicht etwas, das wir lesen — weshalb [die MUSH-Spiele](/games?lineage=MUSH) überall, wo sie +auftauchen, als *abgeleitet* gekennzeichnet sind. + +## MUCK + +Ein TinyMUD-Abkömmling wie MUSH, mit eigenem Softcode (MUF, einer Forth-artigen Sprache) und einer +starken Tradition sozialer Welten und der Furry-Fandom-Welten. Technisch nah an MUSH; kulturell so +eigen, dass Leute, die beides spielen, sie nicht als dasselbe beschreiben würden — +[die MUCK-Spiele](/games?lineage=MUCK). + +## MOO + +*MUD, Object-Oriented*. Der reinste Ausdruck der Idee „das Spiel bearbeitet sich selbst“: Nahezu +alles in einem MOO ist in der Programmiersprache MOO geschrieben, von den Leuten, die es benutzen, +von innen heraus. LambdaMOO ist der Vorfahr, und MOOs waren historisch in Bildung und Forschung +ebenso beliebt wie im Spiel. [Die MOO-Spiele](/games?lineage=MOO). + +## Was sollten Sie also tatsächlich fragen? + +Drei Fragen leisten mehr als das Wort aus vier Buchstaben: + +1. **Gibt es Kampf, und ist er automatisiert?** Das trennt die Diku/LP-Linie verlässlicher von der + TinyMUD-Linie als jeder Name. +2. **Wer baut?** Nur die Spielleitung, oder jeder mit einem Bau-Bit? +3. **Ist das Spiel verabredet oder beiläufig?** Szenen nach Termin und gepostes Rollenspiel, oder + einloggen und los? + +Das Verzeichnis auf dieser Website kann Ihnen einen Teil der ersten Frage beantworten: Die +**Codebase**, die wir für ein Spiel gemessen haben, sagt Ihnen, aus welcher Tradition sein Server +kommt, und die Facette **Abstammung** ist dieselbe Antwort, filterbar gemacht. Die Kultur kann sie +Ihnen nicht sagen, und diese Seite wird nicht so tun als ob. + +Eine Warnung zu dieser Facette, denn auf dieser Seite wird jemand ihr zum ersten Mal begegnen. Die +Codebase ist gemessen und die Abstammung nicht: Sie ist *unsere* Gruppierung dessen, was ein Spiel +uns mitgeteilt hat, geführt unter einer eigenen Kennzeichnung — **abgeleitet** — neben *gemessen* und +*angegeben*. Wo eine Codebase keinen unumstrittenen Vorfahren hat, wird sie aus jeder Abstammung +herausgelassen statt unter der nächstbesten einsortiert, und mehrere dieser Spiele stimmen uns mit +eigenen Worten zu und veröffentlichen `FAMILY Custom`. diff --git a/content/reference/de/protocol-atcp.md b/content/reference/de/protocol-atcp.md new file mode 100644 index 0000000..da52189 --- /dev/null +++ b/content/reference/de/protocol-atcp.md @@ -0,0 +1,31 @@ +--- +kind: protocol +slug: atcp +title: ATCP +summary: Der Vorgänger von GMCP. Out-of-Band-Daten mit lockererer Nutzlast, weitgehend abgelöst und immer noch von Servern ausgehandelt, die es nie entfernt haben. +protocol: ATCP +see-also: protocols/gmcp +see-also: protocols/msdp +see-also: clients/mudlet +--- + +ATCP — das Achaea Telnet Client Protocol — ist Telnet-Option 200, und hier wurde die Idee, neben dem +MUD-Text strukturierte Daten zu senden, zum ersten Mal breit eingesetzt. Ein Server sendet einen +Modulnamen und eine Nutzlast; der Client leitet sie weiter. + +Sein Format für die Nutzlast ist lockerer als das JSON von [GMCP](/reference/protocols/gmcp), und im +Wesentlichen ist das der Grund, warum GMCP es abgelöst hat. Clients, die ATCP unterstützen, führen es +heute in der Regel als veraltet und verweisen stattdessen auf GMCP. + +## Warum es immer noch da ist + +Weil nichts kaputtgeht, wenn man es angeschaltet lässt. Ein Server, der ATCP 2008 implementiert und +2014 GMCP ergänzt hat, handelt meist immer noch beides aus, und ein Client, der beides unterstützt, +nimmt das, was ihm angeboten wird. + +Für eine neue Implementierung gibt es keinen Grund, es zu wählen. + +## Was wir messen + +Server, die Telnet-Option 200 in einem von uns beobachteten Handshake angeboten haben. Ein niedriger +Wert ist hier zu erwarten und sagt etwas über das Alter aus und sonst über nichts. diff --git a/content/reference/de/protocol-charset.md b/content/reference/de/protocol-charset.md new file mode 100644 index 0000000..b065e42 --- /dev/null +++ b/content/reference/de/protocol-charset.md @@ -0,0 +1,43 @@ +--- +kind: protocol +slug: charset +title: CHARSET +summary: Die Telnet-Option aus RFC 2066, mit der eine Kodierung vereinbart wird. Der Grund, warum die Namen eines Spiels ihre Akzente über die Strecke retten, und die Ursache einiger subtiler Fehler, wenn sie fehlt. +protocol: CHARSET +home: https://www.rfc-editor.org/rfc/rfc2066 +see-also: protocols/ttype +see-also: connecting +see-also: codebases/tinymux +--- + +CHARSET ist Telnet-Option 42, festgelegt in RFC 2066. Die eine Seite bietet eine Liste von +Zeichensätzen an, die andere wählt einen aus, und beide sind sich danach einig, wie Bytes auf Zeichen +abgebildet werden. + +In der Praxis einigt sich die Aushandlung auf **UTF-8** oder findet gar nicht erst statt. Die +MUSH-Familie handelt sie merklich häufiger aus als die MUD-Familie — TinyMUX, RhostMUSH und PennMUSH +tun es alle —, was eine Bevölkerung widerspiegelt, die Prosa schreibt, in der Namen vorkommen. + +## Was ohne sie passiert + +Ein Client muss raten, und geraten wird üblicherweise entweder ASCII oder Latin-1. Rät er ASCII, wird +jedes Byte oberhalb von 0x7F zu einem Fragezeichen; rät er Latin-1 bei einem UTF-8-Server, wird aus +jedem Zeichen mit Akzent ein Paar Satzzeichen. Beide Fehler sehen aus wie ein Fehler des Spiels und +sind keiner. + +Für einen Crawler wird das an einer bestimmten Stelle unangenehm. Unsere eigene Telnet-Bibliothek +setzt ihre aktuelle Kodierung standardmäßig auf ASCII, und dieser Standard ist nicht folgenlos — mit +ihm wird jedes Byte dekodiert, bei jedem Server, der CHARSET nie aushandelt, und das sind die +meisten. Aus diesem Grund geben wir ihn bewusst selbst vor. + +## Die eine Stelle, die CHARSET nicht erreicht + +MSSP-Feldnamen und -Werte werden als ASCII dekodiert, gleich worauf sich CHARSET geeinigt hat, denn +eine Subnegotiation ist ein Befehl und kein Text, und die Spezifikation begrenzt CHARSET auf Text. +Das ist vertretbar konform und es ist verlustbehaftet: Ein Spiel, dessen MSSP-`NAME` `Café Noir` +lautet, meldet `Caf? Noir`, und die ursprünglichen Bytes sind weg, bevor irgendetwas unter unserer +Kontrolle sie zu sehen bekommt. + +Wenn Sie auf dieser Website in einem angegebenen Feld ein verstümmeltes Zeichen sehen und in der +Ausgabe des Spiels selbst nicht, dann ist das der Grund, und von unserer Seite lässt es sich nicht +wiederherstellen. diff --git a/content/reference/de/protocol-gmcp.md b/content/reference/de/protocol-gmcp.md new file mode 100644 index 0000000..14cf678 --- /dev/null +++ b/content/reference/de/protocol-gmcp.md @@ -0,0 +1,46 @@ +--- +kind: protocol +slug: gmcp +title: GMCP +summary: Das Generic Mud Communication Protocol — strukturierte JSON-Nachrichten neben dem Text und der Out-of-Band-Kanal, gegen den die meisten modernen Clients entwickeln. +protocol: GMCP +home: https://www.mudhalla.net/tintin/protocols/gmcp/ +see-also: protocols/msdp +see-also: protocols/atcp +see-also: clients/mudlet +--- + +GMCP ist Telnet-Option 201. Ist sie einmal ausgehandelt, kann der Server **strukturierte Daten out of +band** senden: einen Paketnamen und eine JSON-Nutzlast, die im selben Strom ankommen wie der Text, +aber nicht Teil von ihm sind. + +`Char.Vitals { "hp": 412, "maxhp": 500 }` ist das kanonische Beispiel. Ein Client kann daraus eine +Lebensanzeige speisen, ohne die Prosa nach Zahlen abzugrasen, und genau darum geht es — eine +Statusanzeige, die auf Mustererkennung im Text beruht, geht an dem Tag kaputt, an dem ein Spiel +seinen Prompt ändert, und eine auf GMCP gebaute nicht. + +Der Namensraum der Pakete beruht auf Konvention und nicht auf einem Standard. `Char`, `Room`, `Comm` +und `Client` sind weit verbreitet; darüber hinaus erfinden Spiele, was sie brauchen, und einem Client +muss in der Regel gesagt werden, was ein bestimmtes Spiel sendet. + +## Warum es ATCP verdrängt hat + +GMCP ist der Nachfolger von [ATCP](/reference/protocols/atcp), das dieselbe Aufgabe mit einem +lockereren Nutzlastformat erledigte. JSON war die Verbesserung, und Mitte der 2010er-Jahre war der +Umstieg weitgehend vollzogen. Ein Spiel, das beides unterstützt, ist nichts Ungewöhnliches; ein neues +Spiel, das nur ATCP unterstützt, wäre es. + +## Was wir messen + +Ein Spiel zählt hier, wenn **sein Server GMCP in einem von uns beobachteten Handshake angeboten +hat**. Das ist eine andere Behauptung als ein MSSP eines Spiels, das `GMCP 1` sagt — worauf die +meisten Protokolltabellen in diesem Hobby aufgebaut sind —, und die beiden widersprechen sich +regelmäßig. + +Eine Anmerkung zur Messung aus unserer eigenen Geschichte: Eine Zeit lang konnten wir GMCP auf +Servern nicht sehen, die auch [MCCP](/reference/protocols/mccp) aushandelten, denn unsere +Telnet-Bibliothek handelte die Kompression aus, ohne den Strom zu dekomprimieren, und alles nach der +Kompressionsmarke war für uns Rauschen. Von mindestens einem Server in unserer Erhebung stellte sich +heraus, dass er die ganze Zeit GMCP sprach. Wenn ein Wert auf dieser Seite für eine Familie niedrig +aussieht, die Sie gut kennen, ist diese Art von Defekt das Erste, was zu vermuten ist — bei uns, +nicht bei ihnen. diff --git a/content/reference/de/protocol-mccp.md b/content/reference/de/protocol-mccp.md new file mode 100644 index 0000000..21044e5 --- /dev/null +++ b/content/reference/de/protocol-mccp.md @@ -0,0 +1,44 @@ +--- +kind: protocol +slug: mccp +title: MCCP +summary: Kompression des Datenstroms. Billig, weit verbreitet und das Protokoll, das den lehrreichsten Fehler in der Geschichte dieses Projekts hervorgebracht hat. +protocol: MCCP +home: https://www.mudhalla.net/tintin/protocols/mccp/ +see-also: codebases/rom +see-also: codebases/dikumud +see-also: protocols/gmcp +--- + +MCCP komprimiert den Strom vom Server zum Client mit zlib. Version 1 ist Telnet-Option 85 und +praktisch historisch; **Version 2** ist Option 86 und das, was moderne Server aushandeln. Nachdem der +Server `IAC SB MCCP2 IAC SE` gesendet hat, gehört jedes folgende Byte zu einem einzigen durchgehenden +zlib-Strom. + +Bei einem Textprotokoll ist das eine echte Ersparnis — MUD-Ausgabe lässt sich außerordentlich gut +komprimieren —, und in der Diku- und der LP-Familie ist es verbreitet: Rund ein Drittel der von uns +erhobenen Codebases handelt es aus. + +## Der Fehlerfall, und warum er hier zählt + +Ein Client, der MCCP2 aushandelt und den Strom dann nicht dekomprimiert, empfängt **ab der +Kompressionsmarke binären Müll**. Kein Fehler, kein Verbindungsabbruch: Der Verbindungsbildschirm +kommt als Wand aus Ersatzzeichen an, und alles danach — die `WHO`-Antwort, jedes spätere MSSP, die +ganze Sitzung — ist verloren. + +Das ist nicht hypothetisch. Unsere eigene Telnet-Bibliothek hat genau das getan. Sie handelte die +Option aus, löste ihren Callback „Kompression aktiviert“ aus und dekomprimierte kein einziges Byte. +Die Nutzlast ließ sich mit einem gewöhnlichen zlib-Aufruf sauber entpacken, und das machte es +eindeutig, dass die Server im Recht waren und wir nicht. Dreizehn der achtunddreißig Codebases in +unserer Erhebung waren betroffen, und solange das andauerte, konnten wir nicht beobachten, was diese +Server *nach* dem Beginn der Kompression aushandelten — unsere Aufzeichnung ihrer Fähigkeiten blieb +also hinter dem zurück, was sie konnten. + +Es wurde upstream behoben. Ein Folgedefekt — der Inflater wird pro Lesevorgang neu erzeugt, statt für +die Verbindung erhalten zu bleiben, was mitten in einem großen Verbindungsbildschirm scheitert — ist +gemeldet und offen und betrifft das Ende der größten Bildschirme. + +Zwei Dinge sollte ein Leser daraus mitnehmen. **Ein Protokollwert auf dieser Seite ist ebenso sehr +eine Messung unseres Crawlers wie eine des Hobbys**, und wo wir wissen, dass er falsch war, sagen wir +es. Und wenn Sie einen Client schreiben: MCCP auszuhandeln ist leicht, die Arbeit steckt darin, es +richtig zu dekomprimieren. diff --git a/content/reference/de/protocol-msdp.md b/content/reference/de/protocol-msdp.md new file mode 100644 index 0000000..68a3065 --- /dev/null +++ b/content/reference/de/protocol-msdp.md @@ -0,0 +1,31 @@ +--- +kind: protocol +slug: msdp +title: MSDP +summary: Das Mud Server Data Protocol — dieselbe Aufgabe wie GMCP, erledigt mit einer kompakten binären Kodierung und einem Mechanismus zur Erkundung, den GMCP nicht hat. +protocol: MSDP +home: https://www.mudhalla.net/tintin/protocols/msdp/ +see-also: protocols/gmcp +see-also: clients/tintin +see-also: clients/blightmud +--- + +MSDP ist Telnet-Option 69 und löst dasselbe Problem wie [GMCP](/reference/protocols/gmcp): +strukturierte Daten neben dem Text zu senden, damit ein Client die Prosa nicht nach Zahlen abgrasen +muss. + +Die Unterschiede sind zwei. Die Kodierung von MSDP ist **binär und kompakt** — Variablen und Werte +werden mit einzelnen Steuerbytes markiert statt in JSON verpackt —, und MSDP definiert ein Gespräch +zur **Erkundung**: Ein Client kann mit `LIST` nach `COMMANDS`, `REPORTABLE_VARIABLES` und so weiter +fragen und bekommt gesagt, was ein bestimmtes Spiel unterstützt. GMCP hat dafür kein Gegenstück, +weshalb ein GMCP-Client in der Regel je Spiel konfiguriert werden muss. + +In der Praxis hat GMCP sich in der Verbreitung durchgesetzt, und MSDP hält sich in den Servern und +Clients, die es implementiert haben, oft neben GMCP. + +## Was wir messen + +Ein Spiel zählt hier, wenn sein Server MSDP in einem von uns beobachteten Handshake angeboten hat. +Wie bei jedem Wert in diesem Abschnitt ist das eine positive Beobachtung, und der Rest ist nicht ihr +Gegenteil — ein Spiel, das nicht gezählt ist, implementiert MSDP womöglich nicht, oder wir haben +seinen Handshake einfach noch nicht gelesen. diff --git a/content/reference/de/protocol-msp.md b/content/reference/de/protocol-msp.md new file mode 100644 index 0000000..9537fbb --- /dev/null +++ b/content/reference/de/protocol-msp.md @@ -0,0 +1,41 @@ +--- +kind: protocol +slug: msp +title: MSP +summary: Das MUD Sound Protocol — der Server nennt eine Klangdatei, und der Client spielt sie ab. Alt, einfach und leicht mit zwei anderen Dingen zu verwechseln. +protocol: MSP +home: https://www.zuggsoft.com/zmud/msp.htm +see-also: protocols/mxp +see-also: clients/vipmud +--- + +Mit MSP kann ein Server einen Client bitten, einen Klang abzuspielen: eine in Klammern gesetzte +Anweisung, die eine Datei, eine Lautstärke, eine Wiederholungszahl und eine URL nennt, von der sie zu +holen ist, falls der Client sie nicht hat. Es wird über Telnet-Option 90 ausgehandelt und kann von +Servern, die überhaupt nichts aushandeln, auch in band im Textstrom gesendet werden. + +Es ist wirklich alt und wird wirklich noch benutzt — Umgebungsklang in einem Textspiel wirkt stärker, +als es klingt, und für Spieler, die die Audiohinweise eines Clients statt seiner Anzeige nutzen, ist +er mehr als Zierde. + +## Drei Dinge, die es nicht ist + +Die Client-Tabellen in diesem Abschnitt mussten hier vorsichtig sein, und es lohnt sich +aufzuschreiben, warum: + +- **MCMP** — das Mud Client Media Protocol — ist ein anderes Protokoll mit einer ähnlichen Aufgabe. + Mindestens ein Client implementiert MCMP und nicht MSP, und das eine als das andere zu lesen würde + eine Behauptung in eine Tabelle setzen, die niemand aufgestellt hat. +- **Der eigene Skriptaufruf eines Clients zum Abspielen eines Klangs** ist nicht MSP. Er spielt eine + lokale Datei ab, wenn ein Skript es sagt; MSP ist ein Server, der einem Client sagt, was er + abspielen soll. +- **Unterstützung durch ein mitgeliefertes Plugin ist als solche zu benennen.** Bei einem Client + kommt die MSP-Unterstützung als Plugin, das ausdrücklich keine Telnet-Aushandlung betreibt; das + funktioniert bei Servern, die MSP in band senden, und nicht bei Servern, die erwarten, es + auszuhandeln. + +## Was wir messen + +Server, die Telnet-Option 90 anbieten. Weil MSP häufig ohne Aushandlung in band gesendet wird, liegt +dieser Wert um einen Betrag unter der tatsächlichen Verbreitung, den wir nicht schätzen können — das +ist eine Grenze dessen, was ein Handshake sehen kann, und kein Befund über das Protokoll. diff --git a/content/reference/de/protocol-mssp.md b/content/reference/de/protocol-mssp.md new file mode 100644 index 0000000..c666ea1 --- /dev/null +++ b/content/reference/de/protocol-mssp.md @@ -0,0 +1,61 @@ +--- +kind: protocol +slug: mssp +title: MSSP +summary: Das Mud Server Status Protocol — wie ein Spiel einem Crawler von sich erzählt. Alles, was es meldet, ist angegeben und nicht gemessen, und diese Website hält beides auseinander. +protocol: MSSP +home: https://www.mudhalla.net/tintin/protocols/mssp/ +see-also: protocols/gmcp +see-also: codebases/dikumud +see-also: codebases/pennmush +--- + +MSSP ist Telnet-Option 70. Ein Crawler sendet `IAC DO MSSP`; ein Server, der es unterstützt, +antwortet mit einer Tabelle aus Name/Wert-Paaren, die ihn beschreibt — Name, Spielerzahl, Codebase, +Uptime, Hostname, Port, Genre und was er sonst noch veröffentlichen möchte. + +Es ist das, was diesem Hobby am nächsten an einen maschinenlesbaren Verzeichniseintrag herankommt, +und es ist der Grund, warum es mehrere Verzeichnisse überhaupt gibt. + +## Alles in einem MSSP-Bericht ist eine Behauptung + +Das ist der Punkt, in dem sich diese Website von jedem etablierten Verzeichnis unterscheidet. Ein +MSSP-Bericht ist das Spiel, das Ihnen *von sich erzählt*. `GMCP 1` in einer MSSP-Tabelle heißt, dass +jemand eine `1` in eine Konfigurationsdatei getippt hat, womöglich im Jahr 2011. Es ist kein Beleg +dafür, dass der Server GMCP anbietet, und die beiden widersprechen sich oft genug, um interessant zu +sein. + +Deshalb werden Tatsachen aus MSSP hier als **angegeben** ausgewiesen, und wo wir dieselbe Tatsache +messen können — eine Fähigkeit, indem wir sehen, ob die Option tatsächlich ausgehandelt wird —, wird +beides nebeneinander gezeigt, jedes mit seinem Alter. Ein Spiel, dessen MSSP seit sechs Jahren GMCP +angibt und es kein einziges Mal in einem Handshake angeboten hat, ist eine Tatsache, die man kennen +sollte, und nirgendwo sonst ist sie zu finden. + +Das eine Feld, das wir bewusst überhaupt nicht anrechnen, ist `CREATED`. Es ist eine einzelne von +Hand getippte Zeile, und sie irgendwo anzurechnen würde das Betreffende trivial manipulierbar machen. + +## Wer darauf antwortet + +MSSP ist die Antwort der **Diku- und LP-Welt**. In unserer eigenen Erhebung über 38 Codebases +veröffentlichten 28 eine Spielerzahl über MSSP und sieben über ein `WHO` am Anmeldebildschirm, und +nur zwei taten beides — die zwei Familien sind nahezu disjunkt. AresMUSH, TinyMUX, MUCK, RhostMUSH, +CobraMUSH und TinyMUSH bieten überhaupt kein MSSP. + +Das ist der empirische Grund dafür, vier Schichten abzufragen statt einer: **Ein Crawler, der allein +auf MSSP baut, kann den größten Teil der MUSH-Familie nicht sehen**, und die ist ein großer Teil des +Hobbys und der größte Teil des Publikums, für das diese Website gedacht ist. + +## Fragen, nicht warten + +Sehr viele Server, die MSSP vollständig unterstützen, bieten es von sich aus nie an — sie antworten +auf `IAC DO MSSP` und sagen sonst nichts. Ein Crawler, der mit `IAC WILL NAWS` eröffnet und wartet, +meldet diese Spiele daher als solche, die nichts veröffentlichen, und das ist eine Behauptung über +den Server, gemacht aus dem eigenen Schweigen des Crawlers. Wir senden `IAC DO MSSP` beim Verbinden. + +## Die Klartextform + +Es gibt eine ältere Variante, bei der ein Client am Anmeldebildschirm wörtlich die Zeile +`MSSP-REQUEST` sendet. Wir haben sie gemessen: Von zwanzig versuchten Spielen antworteten drei — und +alle drei antworteten auch auf Telnet-Option 70, sie erreichte also nichts, was die Option nicht +schon erreichte. Acht Server lasen die Anfrage als **Charakternamen** und sagten das auch, wobei +einer der Anmeldeversuche verbraucht wurde, die einem Fremden zustehen. Wir senden sie nicht. diff --git a/content/reference/de/protocol-mxp.md b/content/reference/de/protocol-mxp.md new file mode 100644 index 0000000..322215d --- /dev/null +++ b/content/reference/de/protocol-mxp.md @@ -0,0 +1,42 @@ +--- +kind: protocol +slug: mxp +title: MXP +summary: Das MUD eXtension Protocol — HTML-ähnliche Auszeichnung im Textstrom, die anklickbare Links, Bilder und Formulare möglich macht. Umfassend spezifiziert, ungleichmäßig implementiert. +protocol: MXP +home: https://www.zuggsoft.com/zmud/mxp.htm +see-also: protocols/pueblo +see-also: clients/mushclient +see-also: clients/mudlet +--- + +MXP bettet eine kleine, HTML-ähnliche Auszeichnungssprache in den Text ein, den ein Server sendet: +`` für einen anklickbaren Befehl, `` für einen Link, Elemente für Farbe und Schrift und +einen Mechanismus, mit dem ein Server eigene Tags definieren kann. Es wird über Telnet-Option 91 +ausgehandelt. + +Sein Entwurfsproblem steckt in ihm selbst und ist interessant: Die Auszeichnung reist im selben Strom +wie der Text, ein Server muss also auf Text achtgeben, der wie Auszeichnung *aussieht*, und ein +Client muss achtgeben, was er darstellt. Genau deshalb definiert MXP Sicherheitsstufen — ein Tag, das +in einer Chatzeile von einem anderen Spieler ankommt, ist nicht dasselbe wie ein Tag, das der Server +selbst ausgegeben hat. + +## Anklickbarkeit ist der Grund, warum man es haben will + +Das meiste, wofür MXP tatsächlich benutzt wird, ist, `north` und Gegenstandsnamen in etwas zu +verwandeln, das man anklicken kann. Für einen neuen Spieler ist das ein erheblicher Unterschied, und +deshalb wird das Protokoll trotz seiner Komplexität immer wieder implementiert. + +## Pueblo ist das andere + +[Pueblo](/reference/protocols/pueblo) ist älter als MXP und erledigt eine ähnliche Aufgabe mit einem +anderen, buchstäblicher an HTML angelehnten Ansatz. Ein Client, der das eine unterstützt, unterstützt +häufig das andere nicht, und beim Lesen einer Funktionsliste sind die beiden leicht zu verwechseln — +ein Fehler, vor dem wir uns in den Client-Tabellen dieses Abschnitts hüten mussten. + +## Was wir messen + +Server, die Telnet-Option 91 in einem von uns beobachteten Handshake angeboten haben. MXP wird +seltener ausgehandelt als die Out-of-Band-Protokolle, zum Teil deshalb, weil ein großer Teil seines +Nutzens von Servern eingelöst wird, die die Auszeichnung einfach senden und hoffen, ohne überhaupt +auszuhandeln — und das können wir nicht sehen. diff --git a/content/reference/de/protocol-pueblo.md b/content/reference/de/protocol-pueblo.md new file mode 100644 index 0000000..6052857 --- /dev/null +++ b/content/reference/de/protocol-pueblo.md @@ -0,0 +1,33 @@ +--- +kind: protocol +slug: pueblo +title: Pueblo +summary: Das ältere Verfahren, HTML in ein MUD zu bringen, aus dem gleichnamigen Client. Von Clients der MUSH-Seite weiterhin unterstützt und regelmäßig mit MXP verwechselt. +protocol: PUEBLO +home: https://pueblo.sourceforge.net/ +see-also: protocols/mxp +see-also: clients/beipmu +--- + +Pueblo ging Mitte der Neunziger aus dem gleichnamigen Client hervor und verfolgte einen direkten +Ansatz, MUD-Text aufzuwerten: Der Server soll **HTML** senden und der Client es darstellen. Ein +Server kündigt seine Pueblo-Unterstützung beim Verbinden in einer Zeile an; der Client antwortet, und +von da an darf der Strom Auszeichnung tragen. + +Es erreichte die MUSH-Seite des Hobbys stärker als die MUD-Seite, und MUSH-Server, die es +unterstützen, tun das in der Regel weiterhin. + +## Nicht MXP + +[MXP](/reference/protocols/mxp) ist das spätere Verfahren und das breiter implementierte. Sie +erledigen eine ähnliche Aufgabe und sind nicht kompatibel, und die Pueblo-Unterstützung eines Clients +als MXP-Unterstützung zu lesen — oder umgekehrt — ist der mit Abstand am leichtesten zu machende +Fehler, wenn man einen Client-Vergleich zusammenstellt. Deshalb halten die Client-Seiten in diesem +Abschnitt beides getrennt, und wo ein Projekt das eine dokumentiert und das andere nicht, steht beim +anderen *unbekannt*. + +## Was wir messen + +Der Handshake von Pueblo ist keine Telnet-Option im üblichen Sinn; was wir beobachten, ist also enger +als bei den ausgehandelten Protokollen, und ein niedriger Wert ist hier als Aussage über unsere Sicht +zu lesen und nicht über die Verbreitung. diff --git a/content/reference/de/protocol-tls.md b/content/reference/de/protocol-tls.md new file mode 100644 index 0000000..97d91a7 --- /dev/null +++ b/content/reference/de/protocol-tls.md @@ -0,0 +1,42 @@ +--- +kind: protocol +slug: tls +title: TLS +summary: Verschlüsselte Verbindungen. Meist ein eigener Port statt einer ausgehandelten Umstellung, und die eine Fähigkeit auf dieser Website, die wir durch Verbinden prüfen und nicht durch Fragen. +protocol: TLS +see-also: connecting +see-also: protocols/charset +see-also: clients/potato +--- + +Telnet ist Klartext. Alles, was Sie an ein MU\* senden — auch Ihr Passwort —, quert das Netz lesbar +für alles auf dem Weg, sofern das Spiel nicht TLS anbietet. + +In diesem Hobby heißt TLS fast immer **ein zweiter Port, der vom ersten Byte an TLS spricht**, und +nicht eine Umstellung im laufenden Strom. Ein Spiel mit einem einfachen Port auf 4201 und einem +TLS-Port auf 4202 ist die übliche Form. Es gibt eine ausgehandelte Variante, und sie ist selten +genug, dass die Dokumentation mindestens eines Clients ausdrücklich sagt, sie werde nicht +unterstützt. + +## Warum die Spielseiten das besonders kennzeichnen + +TLS ist die eine Fähigkeit auf dieser Website, die dadurch feststeht, dass wir es *getan* haben: Ein +Endpunkt ist als TLS gekennzeichnet, weil wir gegen ihn einen TLS-Handshake abgeschlossen haben. Da +wird nichts gefragt, und es gibt kein Feld, in dem sich etwas angeben ließe — das macht es zur +saubersten Messung im Katalog. + +Das ist auch der Grund, warum der TLS-Port eines Spiels und sein einfacher Port als getrennte +Endpunkte geführt und nicht zusammengelegt werden. Es sind verschiedene Messungen verschiedener +Dinge. + +## Praktischer Rat + +Wenn ein Spiel, das Sie spielen, einen TLS-Port anbietet, benutzen Sie ihn. Wenn nicht und es Ihnen +wichtig ist, fragen Sie nach — für einen Administrator ist es wenig Arbeit, und dass es ihn nicht +überall gibt, liegt meist daran, dass niemand gefragt hat, und nicht daran, dass jemand etwas dagegen +hätte. + +Prüfen Sie, ob Ihr Client es unterstützt, bevor Sie sich darauf verlassen. Mehrere im Abschnitt +[Clients](/reference) tun es; mindestens einer dokumentiert stattdessen einen Behelf mit einem +externen `stunnel`-Prozess, der funktioniert und mehr Einrichtung ist, als die meisten Leute auf sich +nehmen. diff --git a/content/reference/de/protocol-ttype.md b/content/reference/de/protocol-ttype.md new file mode 100644 index 0000000..3aba86f --- /dev/null +++ b/content/reference/de/protocol-ttype.md @@ -0,0 +1,46 @@ +--- +kind: protocol +slug: ttype +title: TTYPE und MTTS +summary: Wie ein Client einem Server sagt, was er ist und was er kann — auch, sofern der Client sich dafür entscheidet, dass ein Screenreader im Einsatz ist. +protocol: TTYPE +home: https://www.mudhalla.net/tintin/protocols/mtts/ +see-also: protocols/charset +see-also: clients/tintin +see-also: clients/blightmud +--- + +TTYPE ist Telnet-Option 24, aus RFC 1091: Der Server fragt den Client, was für ein Terminal er ist, +und der Client antwortet. Historisch lautete die Antwort `VT100` oder `ANSI`. + +**MTTS** — der Mud Terminal Type Standard — legt darüber eine Konvention. Ein Client antwortet +dreimal: mit seinem Namen, mit seinem Terminaltyp und dann mit `MTTS `, wobei die Bits +Fähigkeiten angeben. 256 Farben, True Color, UTF-8, MNES, MSP über den Out-of-Band-Kanal — und, +bemerkenswert, **`MTTS_SCREEN_READER`**. + +## Das Screenreader-Bit + +Bei dem letzten lohnt es sich innezuhalten, denn es ist die einzige Stelle im Protokollstapel dieses +Hobbys, an der Barrierefreiheit ein Konzept erster Klasse ist. + +Ein Client, der es setzt, teilt dem Server mit, dass ein Screenreader im Einsatz ist, und ein Server, +der das bemerkt, kann sich anpassen: ASCII-Grafik unterdrücken, den schmückenden Rahmen aus +Linienzeichen um eine Raumbeschreibung weglassen, eine Tabelle anders anordnen. Sowohl +[TinTin++](/reference/clients/tintin) als auch [Blightmud](/reference/clients/blightmud) geben es an, +und [Mudlet](/reference/clients/mudlet) hat eine Einstellung dafür. + +Ob ein bestimmtes Spiel darauf reagiert, ist eine andere Frage, und keine, die diese Website messen +kann — wir können einen Server nicht fragen, was er anders machen würde. + +## Was ein Crawler hier schuldet + +Ein Crawler weist sich über TTYPE aus, und das soll er auch. Unserer tut es, mit einer URL zur +Information, damit ein Administrator beim Lesen seiner Logs herausfinden kann, wer sich mit seinem +Spiel verbunden hat und wie er uns bitten kann aufzuhören. Ein Crawler, der `ANSI` antwortet und +sonst nichts, ist von Haus aus anonym, und dafür gibt es keinen guten Grund. + +## Was wir messen + +Server, die TTYPE mit uns ausgehandelt haben. Beachten Sie, dass dies eine der wenigen Optionen ist, +bei denen *wir* die gefragte Seite sind; ein Wert hier ist also eine Zählung der Server, die +überhaupt gefragt haben. diff --git a/content/reference/ja/client-atlantis.md b/content/reference/ja/client-atlantis.md new file mode 100644 index 0000000..730bf87 --- /dev/null +++ b/content/reference/ja/client-atlantis.md @@ -0,0 +1,46 @@ +--- +kind: client +slug: atlantis +title: Atlantis +summary: macOS専用のクライアント。長命で、長らくベータのままです。スクリプト機能はもう動かないと文書化されており、それがこの節で唯一の正直な「いいえ」です。 +home: https://www.riverdark.net/atlantis/ +platform: macOS +capability: screen reader | unknown | +capability: TLS | yes | https://www.riverdark.net/atlantis/history.php +capability: UTF-8 | yes | https://www.riverdark.net/atlantis/history.php +capability: MCCP | yes | https://www.riverdark.net/atlantis/history.php +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | no | https://www.riverdark.net/atlantis/ +see-also: clients/mudlet +see-also: protocols/charset +--- + +AtlantisはmacOSのネイティブなクライアントで、Mac OS X 10.3の頃から存在し、Catalinaの時代に64ビットへ +更新されました。RFC 2066の文字セットのネゴシエーションとUnicodeを扱い、これはその年齢から想像される +よりも優れています。MCCPとSSLにも対応しています。 + +## この節で唯一の「いいえ」 + +スクリプト機能はCamelBonesのブリッジを介したPerlでしたが、プロジェクト自身のホームページが、それはもう +動かないと述べています — AppleによるPerlの扱いが変わり、ライブラリの作者は数年前に亡くなりました。 +これは*出典のある不在*であって、不明とは別のものです。そしてクライアントの節全体で、それを持つセルは +ここだけです。ほかのどこでも、正直な答えは「確かめられなかった」でした。 + +## 確かめられなかったすべて + +バージョンの履歴は完全な形で公開されており、**MCCP**、**SSL**、**文字セットのネゴシエーション**に +触れています — そしてGMCP、MSDP、ATCP、MSPには一度も触れていません。MXPは一度だけ、1.0.0より後の +バージョンに向けたものとして現れますが、そのバージョンは来ていません。 + +スクリプトのAPIにはPerlの `Atlantis::Speak()` という呼び出しがあり、これをスクリーンリーダー対応と +読むのは簡単でしょう。そうではありません。プロジェクト自身が動かないと述べているスクリプトの仕組みの +中の、スクリプトから呼ぶ音声読み上げです。VoiceOver、「accessible」、「screen reader」のいずれも、 +ホームページにも、ダウンロードのページにも、完全なバージョン履歴にも、アーカイブされたユーザーガイドにも +現れません。 + +現在のダウンロードは0.9.9.8で、名目上はまだベータであり、リリース日はサイトのどこにも公開されて +いません。 diff --git a/content/reference/ja/client-beipmu.md b/content/reference/ja/client-beipmu.md new file mode 100644 index 0000000..12f3193 --- /dev/null +++ b/content/reference/ja/client-beipmu.md @@ -0,0 +1,47 @@ +--- +kind: client +slug: beipmu +title: BeipMU +summary: この趣味のMUSH側に向けたWindowsのクライアント。出力ウィンドウにスクリーンリーダー対応があり、MXPではなくPuebloに対応します。 +home: https://beipdev.github.io/BeipMU/ +platform: Windows +capability: screen reader | yes | https://github.com/BeipDev/BeipMU/blob/master/Assets/Changes.txt +capability: TLS | yes | https://beipdev.github.io/BeipMU/ +capability: UTF-8 | yes | https://beipdev.github.io/BeipMU/ +capability: MCCP | unknown | +capability: GMCP | yes | https://github.com/BeipDev/BeipMU/blob/master/Documentation/GMCP.md +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://beipdev.github.io/BeipMU/ +see-also: clients/mushclient +see-also: clients/potato +see-also: collaborative-roleplay +--- + +BeipMUはMITライセンスのWindowsのクライアントで、活発にリリースされており、戦闘MUDではなくMUSH風の +遊び方を念頭に作られた数少ないものの一つです — 複数の入力ウィンドウ、スポーンウィンドウ、そして長い +段落を前提としたテキストエンジン。スクリプトは既定でJavaScriptで、ほかのActiveScriptのエンジンも +使えます。 + +## アクセシビリティ + +出力ウィンドウはWindowsの `IAccessible` インターフェースを実装しています。これは視覚に障害のある +プレイヤーにとっての使いやすさへ向けた一歩として、意図的に加えられたものです。また音声読み上げのための +**Speak**というトリガーの動作があります。特定のスクリーンリーダーはどこにも名指しされておらず、 +ドキュメントにアクセシビリティの章はありません。 + +調べに行くなら注意が一つあります。プロジェクト自身のドキュメントのあるページは、BeipMUは音声合成を +使えないと今も述べています。そのページは古くなっています — 変更履歴も、メンテナー自身のissueへの +コメントも、どちらもそれより後のものです。 + +## このクライアントについて起こしやすい2つの誤り + +**BeipMUが実装しているのはMCMPであって、MSPではありません。** 両者は名前も目的も似た、別のプロトコル +です。一方を他方として読めば、誰もしていない主張をこの表に載せることになります。だからMSPの行は不明と +しています。 + +**対応しているのはPuebloであって、MXPではありません。** PuebloはMUDの中でHTMLを使う古いほうの方式で、 +MXPは後のほうです。BeipMUは基本的なPuebloのスタイルとクリックできるリンクを文書化しています。MXPに +ついては、どちらとも確かめられませんでした。 diff --git a/content/reference/ja/client-blightmud.md b/content/reference/ja/client-blightmud.md new file mode 100644 index 0000000..7cd7484 --- /dev/null +++ b/content/reference/ja/client-blightmud.md @@ -0,0 +1,47 @@ +--- +kind: client +slug: blightmud +title: Blightmud +summary: Rustで書かれた現代的なターミナルのクライアント。Luaのスクリプト、内蔵の音声読み上げ、そしてサーバーに自らを知らせるスクリーンリーダーモードを備えています。 +home: https://github.com/Blightmud/Blightmud +platform: Linux +platform: macOS +platform: Windows (WSL only) +capability: screen reader | yes | https://github.com/Blightmud/Blightmud +capability: TLS | yes | https://github.com/Blightmud/Blightmud +capability: UTF-8 | yes | https://github.com/Blightmud/Blightmud +capability: MCCP | yes | https://github.com/Blightmud/Blightmud +capability: GMCP | yes | https://github.com/Blightmud/Blightmud +capability: MSDP | yes | https://github.com/Blightmud/Blightmud +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/Blightmud/Blightmud +see-also: clients/tintin +see-also: clients/mudlet +see-also: protocols/ttype +--- + +BlightmudはRustで書かれたターミナルのクライアントで、GPL 3、そしてこの節で最も活発にリリースされて +いるクライアントの一つです。スクリプトはLuaです。ターミナル専用で、ネイティブのWindowsビルドはなく、 +Windowsの利用者はWSLの下で動かします。 + +## アクセシビリティ + +Blightmudにはここで3つの別々の要素があり、これは表の1行が担える以上のものです。 + +- **スクリーンリーダーに配慮したモード**(`--reader-mode`、または `reader_mode` の設定)。ターミナルの + UIを、リーダーが追える形に変えます。ステータス領域には対応していません。 +- **内蔵の音声読み上げ**。オプションのコンパイルとして提供され、スクリプトから使えるLuaのAPIが付きます + — 一致した行が読み上げられないようにする `tts.gag()` もあります。ドキュメントは、このTTSを + スクリーンリーダーと並べて動かすのが常に幸せな組み合わせとは限らない、と率直に書いています。 +- **MTTSの自動通知**。リーダーモードのとき、またはTTSを有効にしているとき、サーバーへ伝える自分自身の + 情報に `MTTS_SCREEN_READER` を加えるので、気にかけるゲームは対応を変えられます。 + +TinTin++と同じく、特定のスクリーンリーダーは名指しされていないので、これは文書化されたモードであって、 +ある製品との互換性が試験されたということではありません。 + +## 表が不明としている箇所 + +**MXP**、**MSP**、**ATCP**は、プロジェクトのREADMEにも同梱のヘルプにもまったく現れません。**MCCP**は +v2として文書化されており、v1も扱えるかどうかは確かめられませんでした。 diff --git a/content/reference/ja/client-mudlet.md b/content/reference/ja/client-mudlet.md new file mode 100644 index 0000000..6a1f7df --- /dev/null +++ b/content/reference/ja/client-mudlet.md @@ -0,0 +1,54 @@ +--- +kind: client +slug: mudlet +title: Mudlet +summary: クロスプラットフォームでLuaによるスクリプトを備え、この節でスクリーンリーダー対応が最も丁寧に文書化されているクライアント。 +home: https://www.mudlet.org/ +platform: Windows +platform: macOS +platform: Linux +capability: screen reader | yes | https://wiki.mudlet.org/w/Manual:Screen_Readers +capability: TLS | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: UTF-8 | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MCCP | unknown | +capability: GMCP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MSDP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: ATCP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MXP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MSP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: scripting | yes | https://github.com/Mudlet/Mudlet +see-also: clients/blightmud +see-also: clients/tintin +see-also: protocols/gmcp +see-also: connecting +--- + +Mudletはグラフィカルなクライアントで、マッパー、パッケージの仕組み、そして自身の機能の大半がそれに +対して書かれているLuaのAPIを備えています。GPLで、活発にリリースされており、現代の戦闘MUDを始める人への +定番の推薦です。 + +## アクセシビリティ + +これはこの節で最も強い、文書に裏づけられた事例を持つクライアントであり、ここで「文書化されている」が +何を意味するのかを書き出しておく価値があります。珍しいことだからです。 + +Mudletには**スクリーンリーダーについてのマニュアルの章**があり、OSごとのページではWindowsのNarrator、 +NVDA、JAWS、LinuxのOrca、macOSのVoiceOverが名指しされています。クライアント内には `mudlet access on` +というコマンドがあり、届いたゲームのテキストをリーダーを通じて読み上げる選択肢もあります。さらに、 +スクリーンリーダーの使用をMTTSでサーバーへ通知する設定もあるので、望むゲームは対応を変えられます。 + +うまく動かないところについても率直です。自身のWindowsのページは、JAWSは他のリーダーのようには出力 +ウィンドウを読まないと述べ、代わりにNarratorかNVDAを勧めています。自分のアクセシビリティ対応が実用に +ならない場合を公開するプロジェクトは、チェックマークを公開するプロジェクトより良い情報を与えて +くれます。 + +## 表が不明としている箇所 + +**MCCP**。MudletのソースはMCCPのv1とv2を実装していますが、マニュアルの対応プロトコルのページはそれを +挙げておらず、この節の規則は、機能の主張はプロジェクト自身のドキュメントを典拠にする、というものです。 +ヘッダーから定数を読み出すのは同じ行為ではないので、このセルは不明としています。 + +## エンコーディングについての注記 + +Mudletの既定のサーバーデータのエンコーディングはUTF-8ではなくASCIIであり、CHARSETのネゴシエーションは +4.10で入りました。新しいプロファイルでゲームのテキストが化けるなら、まず見るべきはその設定です。 diff --git a/content/reference/ja/client-mushclient.md b/content/reference/ja/client-mushclient.md new file mode 100644 index 0000000..efd3541 --- /dev/null +++ b/content/reference/ja/client-mushclient.md @@ -0,0 +1,51 @@ +--- +kind: client +slug: mushclient +title: MUSHclient +summary: 長く定着しているWindowsのクライアント。5つのスクリプト言語、プロトコル対応の大半が置かれているプラグインのアーキテクチャ、そして緩やかになったリリースの歴史。 +home: https://www.mushclient.com/ +platform: Windows +platform: Linux (Wine) +capability: screen reader | unknown | +capability: TLS | unknown | +capability: UTF-8 | unknown | +capability: MCCP | yes | https://www.mushclient.com/mushclient/mccp.htm +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | yes | https://www.mushclient.com/gmcp +capability: MXP | yes | https://www.mushclient.com/mushclient/doc/general/features.html +capability: MSP | yes | https://github.com/nickgammon/mushclient/blob/master/plugins/msp.xml +capability: scripting | yes | https://www.mushclient.com/mushclient/doc/general/features.html +see-also: clients/mudlet +see-also: clients/potato +see-also: protocols/mccp +--- + +MUSHclientはNick GammonによるWindowsのクライアントで、MITライセンス、そして長い期間、Windowsを使う人に +とっての既定の答えでした。Lua、VBScript、JScript、PerlScript、Pythonでスクリプトが書け、その機能の +多くはコアではなくプラグインが担っています — これは本物のアーキテクチャ上の選択であり、上の表の +いくつもの行が見た目より答えにくい理由でもあります。 + +最後にタグの付いたリリースは**2019年3月の5.06**です。リポジトリには今もコミットが続いており、出荷されて +いない5.07のリリースノートも存在します。 + +## これほど多くの行が不明としている理由 + +どれも正直な答えが「確かめられなかった」という場合であり、その理由はそれぞれ違います。 + +- **GMCP** — プロジェクト自身のこれについてのページが示しているのは、あなたが書けるであろう*例*としての + プラグインであって、クライアントが持つ機能ではありません。それは対応を出荷することとは違うので、 + このセルは「はい」ではなく不明です。 +- **TLS** — 文書化されている方法は外部の `stunnel` プロセスです。OpenSSLによるTLSを加えるコミットは + 2026年にmasterブランチへ入りましたが、どのリリースにも含まれていないので、今日利用者がインストール + できるもので指し示せるものがありません。 +- **UTF-8** — CHARSETのネゴシエーションは未リリースの5.07のノートに現れますが、出荷済みのバージョンの + ドキュメントには、当サイトが探した限りどこにもありません。 +- **MSDP** — どちらとも何もありません。 +- **スクリーンリーダー** — WindowsのSAPIを使う音声読み上げのプラグインがクライアントに同梱されて + いますが、それはスクリーンリーダー対応と同じものではありません。マニュアルにアクセシビリティの節は + なく、作者は自身のフォーラムで、出力ウィンドウがリーダーにとって扱いにくい理由を説明しています。 + 現在行という概念がないのです。答えを確かめられなかったので、表も答えを出していません。 + +このどれも*いいえ*ではありません。いくつかは十分に「はい」でありうるのに、当サイトがそれを示せなかった +というだけです。 diff --git a/content/reference/ja/client-potato.md b/content/reference/ja/client-potato.md new file mode 100644 index 0000000..e9bd8cc --- /dev/null +++ b/content/reference/ja/client-potato.md @@ -0,0 +1,46 @@ +--- +kind: client +slug: potato +title: Potato MUSHclient +summary: MUSHのプレイヤーのために書かれた、クロスプラットフォームのTcl/Tkクライアント。エンコーディングへの対応は良好で、ドキュメントはプロトコルの大半について何も語りません。 +home: https://www.potatomushclient.com/ +platform: Windows +platform: Linux +platform: macOS +capability: screen reader | unknown | +capability: TLS | yes | https://github.com/potatomushclient/potato/wiki/ConfigureWorldsBasics +capability: UTF-8 | yes | https://github.com/potatomushclient/potato/wiki/Features +capability: MCCP | unknown | +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/potatomushclient/potato/wiki/FAQs +see-also: clients/beipmu +see-also: clients/mushclient +see-also: collaborative-roleplay +--- + +PotatoはMUSHでの遊びのために作られたTcl/Tkのクライアントです — 複数のワールド、スポーンウィンドウ、 +そして戦闘のコマンドではなくポーズを打っていることを前提にした既定値の一式。同じソースからWindows、 +Linux、macOSで動き、macOSのビルドはたいてい1つか2つバージョンが遅れています。 + +文字エンコーディングをネゴシエートし、完全なUnicodeを話します。この趣味のMUSH側にとっては、実際上 +これが最も重要な機能です。 + +文書化された制限が一つあります。最初からSSLであるポートへの接続には対応していますが、設定のページ自身 +が、STARTTLS方式のネゴシエートされるSSLには**対応していない**と述べています。 + +## 6つの行が不明としている理由 + +当サイトは、プロジェクトのホームページ、ダウンロードのページ、103個あるウィキのヘルプファイルすべて、 +そしてソースツリー全体を、GMCP、MSDP、MCCP、MXP、MSP、ATCPについて検索しました。そのどれについても、 +文書化された記述はありません。いくつかに触れる*コード*はありますが、この節はコードを機能の主張には +変えません — ヘッダーにある定数を根拠に「はい」と言う表は、プロジェクトが一度もしなかった約束を +することになります。 + +スクリーンリーダーの行も、同じやり方でたどり着いた同じ答えです。プロジェクトが公開しているすべてを +対象に、大文字小文字を区別せず「screen reader」「text-to-speech」、NVDA、JAWS、VoiceOver、 +「accessibility」「visually impaired」「blind」を掃いたところ、何も出てきませんでした。これは +ソフトウェアについての発見ではありません。 diff --git a/content/reference/ja/client-tintin.md b/content/reference/ja/client-tintin.md new file mode 100644 index 0000000..e8e6b00 --- /dev/null +++ b/content/reference/ja/client-tintin.md @@ -0,0 +1,52 @@ +--- +kind: client +slug: tintin +title: TinTin++ +summary: 独自のスクリプト言語を持つターミナルのクライアント。電話機を含むあらゆるプラットフォームで動き、文書化されたスクリーンリーダーモードがあります。 +home: https://tintin.mudhalla.net/ +platform: Linux +platform: macOS +platform: Windows +platform: Android +platform: iOS +capability: screen reader | yes | https://tintin.mudhalla.net/manual/screen_reader.php +capability: TLS | yes | https://github.com/scandum/tintin +capability: UTF-8 | yes | https://github.com/scandum/tintin +capability: MCCP | yes | https://tintin.mudhalla.net/ +capability: GMCP | yes | https://tintin.mudhalla.net/manual/event.php +capability: MSDP | yes | https://tintin.mudhalla.net/manual/msdp.php +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/scandum/tintin +see-also: clients/blightmud +see-also: clients/mudlet +see-also: protocols/msdp +see-also: protocols/ttype +--- + +TinTin++はコマンドラインのクライアントで、GPL 3、活発にリリースされており、ここにある何よりも多くの +場所で動きます — AndroidとiOSも含めて。スクリプト言語は独自のもので、簡潔で、非常に多くのことが +できます。他のクライアントがGUIでやることのかなりの部分が、ここでは `#config` の1行です。 + +同じ作者が**MSSP**と**MSDP**のプロトコル仕様を維持しており、この節のプロトコルのページの多くが同じ +サイトを典拠にしているのは、そのためです。 + +## アクセシビリティ + +TinTin++には**スクリーンリーダーモード**(`#config screen reader on`、または起動時の `-s`)のための +専用のマニュアルページがあります。有効にすると2つのことが起きます。読み上げても意味をなさない視覚的な +要素を取り除くか変えること、そしてスクリーンリーダーの使用を[MTTS](/reference/protocols/ttype)を通じて +サーバーへ伝えることで、ゲームは自分の出力を合わせられます。 + +これは文書化されたモードであって、特定のリーダーで試験したという主張ではありません — そのページに +製品名は挙がっていません。動作するリーダーを名指しするクライアントよりは明確に弱い証拠であり、何も +ないよりは明確に強い証拠です。 + +## 表が不明としている箇所 + +**MXP**と**MSP**はどちらも、プロジェクトのサイトにコミュニティのスクリプトがありますが、スクリプトが +あることはクライアントがそのプロトコルに対応していることではありません — MXPのものは、すべてのMUDで +動くとは限らないとはっきり述べています。どちらについても、ネイティブの対応は確かめられませんでした。 +**ATCP**はどちらとも何も見つかりませんでした。なお、ATCPはおおむねGMCPに取って代わられており、GMCPには +TinTin++が対応しています。 diff --git a/content/reference/ja/client-tinyfugue.md b/content/reference/ja/client-tinyfugue.md new file mode 100644 index 0000000..900328e --- /dev/null +++ b/content/reference/ja/client-tinyfugue.md @@ -0,0 +1,43 @@ +--- +kind: client +slug: tinyfugue +title: TinyFugue +summary: 古典的なUNIXのターミナルクライアント。上流は2007年以降リリースしておらず、メンテナンスされているフォークがそれを前へ運んでいます。 +home: https://tinyfugue.sourceforge.net/ +platform: Linux +platform: macOS +platform: BSD +capability: screen reader | unknown | +capability: TLS | yes | https://tinyfugue.sourceforge.net/ +capability: UTF-8 | unknown | +capability: MCCP | yes | https://tinyfugue.sourceforge.net/ +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://tinyfugue.sourceforge.net/ +see-also: clients/tintin +see-also: clients/blightmud +--- + +TinyFugue — 「tf」 — は、MUSHの世界の大きな部分が20年にわたって使ったターミナルのクライアントで、入力と +出力の別々のペイン、独自のマクロ言語、そして競合のいくつかより長生きした一連の習慣を備えています。 + +**上流は休眠しています**。最後のリリースは2007年1月の5.0 beta 8です。今もビルドが通り、今も動きます。 + +メンテナンスされているフォーク *TinyFugue Rebirth* は活発にリリースされており、GMCP、ATCP、ICUによる +ワイド文字への対応、そして本来のマクロ言語と並ぶPythonとLuaのスクリプトを加えています。上の表が説明して +いるのは**上流**です。「TinyFugue」が指すのはそちらだからです。今日インストールするなら、まずフォークを +見てみる価値があります。 + +## このクライアントのドキュメントにある罠 + +上流のドキュメントには**「non-visual mode」**という項目があります。これは支援技術についてのものでは +なく — 入力を最下行に閉じ込めておくことに関するものです — スクリーンリーダーにも、音声にも、視覚に +障害のある利用者にも、どこにも触れていません。キーワード検索で組み立てた機能表なら、そのファイル名を +「はい」に変えてしまうでしょう。この表は不明としています。ドキュメントが裏づけているのはそこまでだから +です。 + +UTF-8も同じ形の答えです。文書化されているエンコーディングへの対応は8ビットのISO 8859の文字セットに +ついてのもので、UTF-8についての上流の記述は、どちらとも見つかりませんでした。 diff --git a/content/reference/ja/client-vipmud.md b/content/reference/ja/client-vipmud.md new file mode 100644 index 0000000..e3c5319 --- /dev/null +++ b/content/reference/ja/client-vipmud.md @@ -0,0 +1,40 @@ +--- +kind: client +slug: vipmud +title: VIP Mud +summary: 目の見えないプレイヤーのために一から作られた商用のWindowsクライアント。7つのスクリーンリーダーを名指しする一方で、プロトコル対応についてはほとんど何も公開していません。 +home: https://www.gmagames.com/vipmud.shtml +platform: Windows +capability: screen reader | yes | https://www.gmagames.com/vipmud.shtml +capability: TLS | unknown | +capability: UTF-8 | unknown | +capability: MCCP | unknown | +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | yes | https://www.gmagames.com/vipmud.shtml +capability: scripting | yes | https://www.gmagames.com/vipmud.shtml +see-also: clients/mudlet +see-also: clients/blightmud +--- + +VIP Mudは、この節で設計の前提が*まるごと*アクセシビリティであるただ一つのクライアントです。商用で — +30ドル、30日間は全機能を試用でき、その後も機能を減らした形で動き続けます — Windowsのプログラムです。 + +ここでは群を抜いて強いアクセシビリティの主張であり、しかも珍しく具体的です。製品ページは**JAWS、 +Window-Eyes、System Access、NVDA、Cobra、SuperNova/Hal、そしてMicrosoft SAPI**を、そのままで動作する +ものとして名指しし、この問題を真剣に考えた人でなければ出てこない機能を説明しています。ウィンドウごと・ +出力の種類ごとに異なる声、スパムを表示はしたまま読み上げからは外すこと、そしてASCIIアートを抑える +いくつもの方法 — ASCIIアートは、MUDがスクリーンリーダーへ送るもののなかで最も敵対的なものです。 + +## 表の残りが空である理由 + +ベンダーが公開しているのがマニュアルではなくマーケティングのページだからです。そこにはGMCP、MSDP、 +MCCP、MXP、ATCP、TLS、文字エンコーディングへの言及が一つもありません。製品を「a Telnet-based client」と +説明して、そこで終わりです。**不明が9つ並ぶことは、そのソフトウェアへの判定ではありません。** 利用 +できる情報源が1ページしかないときに表がこう見えるというだけであり、それを9つの「いいえ」として公開 +すれば、それらすべてを十分にこなしうる製品についての嘘になります。 + +さらに確かめられなかったことが2つあります。現行バージョンのリリース日と、今も活発に開発されているか +どうかです — ベンダーは2025年2月に買収されており、製品ページには2016年の著作権表示が載っています。 diff --git a/content/reference/ja/codebase-aresmush.md b/content/reference/ja/codebase-aresmush.md new file mode 100644 index 0000000..99f9443 --- /dev/null +++ b/content/reference/ja/codebase-aresmush.md @@ -0,0 +1,33 @@ +--- +kind: codebase +slug: aresmush +title: AresMUSH +summary: Rubyで書かれた現代的なロールプレイ用サーバー。Webのフロントエンドと、softcodeではなく機能として組み込まれたシーン用ツールを備えています。 +codebase: AresMUSH +home: https://aresmush.com/ +see-also: collaborative-roleplay +see-also: codebases/pennmush +see-also: codebases/evennia +--- + +AresMUSHは、**共同ロールプレイ**をまっすぐに狙ったサーバーとしては広く使われているなかで最も新しく、 +自らが後を継ぐTinyMUSHの系統とは異なる立場を取っています。PennMUSHのゲームがシーンシステムや +キャラクターシート、ジョブキューを、その場にいた誰かが書いたsoftcodeから組み上げるのに対し、Aresは +それらを機能として同梱し、ゲームのスタッフにはプログラムするのではなく設定することを期待します。 + +**Webポータル**が付属します — キャラクターのウィキ、シーンのログ、フォーラム、そしてゲーム本体まで、 +すべてブラウザーから到達できます。ログを後から読むことが当たり前のジャンルにとって、これは程度ではなく +種類の違いと言えるほどの差です。 + +設定はYAMLで行い、拡張はRubyのプラグインです。プレイヤー向けのゲーム内プログラミング言語はなく、 +そこが取引になっています。渡されるロープは短く、ロープによる怪我も少なく、そしてMUSHという系統の名前の +由来でもある、即興的な建築の文化もまた少なくなります。 + +## 外から見たときの姿 + +MSSPはありません。ログイン前の `WHO` には応答し、その答えは素の数値ではなく**プレイヤーごとの一覧**で、 +当サイトのパーサーはその構造から数えます。実測したゲームでは、telnetオプションは一つも +ネゴシエートされませんでした。 + +新しいロールプレイのゲームでこれとPennMUSHのどちらを選ぶか迷っているなら、問いはおおよそ、設定する +システムが欲しいのか、それとも書くシステムが欲しいのか、ということになります。 diff --git a/content/reference/ja/codebase-circlemud.md b/content/reference/ja/codebase-circlemud.md new file mode 100644 index 0000000..efe874b --- /dev/null +++ b/content/reference/ja/codebase-circlemud.md @@ -0,0 +1,27 @@ +--- +kind: codebase +slug: circlemud +title: CircleMUD +summary: 教材に使えるほど丁寧に文書化されたDikuMUDの派生。膨大にフォークされ、今も動いています。 +codebase: CircleMUD +home: https://www.circlemud.org/ +see-also: codebases/dikumud +see-also: codebases/tbamud +see-also: codebases/rom +--- + +CircleMUDはDikuMUDの派生ですが、その際立った特徴はゲームの仕組みではありませんでした。**ドキュメント** +でした。Jeremy Elsonのリリースは整っていて、コメントが付き、コーディングガイドが添えられており、その結果、 +人々がCを学び、そこからMUDを動かし、まず何かをリバースエンジニアリングしなくてもフォークできる +コードベースになりました。 + +その帰結として、稼働しているゲームの非常に多くが何世代も離れたCircleの派生であり、しかもプレイヤーの目に +触れる場所にはその名前がどこにも現れないことがよくあります。 + +Circle本体の開発はとうに終わっており、その続きが**tbaMUD**です。今日メンテナンスされているCircleの +ゲームは、たいていtbaMUDとしてメンテナンスされています。 + +## 外から見たときの姿 + +MSSPがあり、要求すれば応答します。ログイン画面に `WHO` はありません — Dikuのファミリーは一般に +それを提供しないので、ログイン画面しか読まないディレクトリはここでは何も見えません。 diff --git a/content/reference/ja/codebase-cobramush.md b/content/reference/ja/codebase-cobramush.md new file mode 100644 index 0000000..df9e654 --- /dev/null +++ b/content/reference/ja/codebase-cobramush.md @@ -0,0 +1,23 @@ +--- +kind: codebase +slug: cobramush +title: CobraMUSH +summary: 独自のdivisionと権限のモデルを持つPennMUSHのフォーク。稼働数は少ないものの、今も応答しています。 +codebase: CobraMUSH +home: https://cobramush.org/ +see-also: codebases/pennmush +see-also: codebases/rhostmush +--- + +CobraMUSHはPennMUSHからフォークし、*division* モデルを加えました。親が使うwizard/royaltyというフラットな +区別に代えて、委譲できる権限を備えた管理権限の階層を置くものです。スタッフの権限をすべて渡すことなく、 +その一部だけを渡したいゲームが、その支持層です。 + +PennMUSH向けに書かれたsoftcodeはたいてい動き、違いはまさにこのフォークの主題だった領域に集中しています。 + +## 外から見たときの姿 + +MSSPはなく、ログイン前の `WHO` は機能し、実測したゲームではtelnetオプションはまったく +ネゴシエートされませんでした。最後の点は批判ではありません。何もネゴシエートしないサーバーは +ネゴシエーションを間違えようのないサーバーであり、素のソケットを流れる素のテキストは、この趣味の +どのクライアントも扱えるものです。 diff --git a/content/reference/ja/codebase-coffeemud.md b/content/reference/ja/codebase-coffeemud.md new file mode 100644 index 0000000..81a202d --- /dev/null +++ b/content/reference/ja/codebase-coffeemud.md @@ -0,0 +1,27 @@ +--- +kind: codebase +slug: coffeemud +title: CoffeeMUD +summary: Javaで書かれたMUDサーバー。当サイトが探査したなかで最大のMSSPレポートを持ち、対応プロトコルの幅も異例の広さです。 +codebase: CoffeeMUD +home: https://www.coffeemud.net/ +see-also: codebases/dikumud +see-also: protocols/mssp +--- + +CoffeeMUDはJavaのMUDサーバーで、機能の面が異例に広く — 自前のWebサーバー、メール、フォーラム、そして +大きなクラスとスキルのシステムを同梱しています — この趣味では数少ない、Cで書かれていないサーバーの +一つです。 + +活発にメンテナンスされており、カタログのこの一帯の基準からすれば、それは声に出して言う価値のあることです。 + +## 外から見たときの姿 + +MSSPと**MCCP2**があります。さらにCoffeeMUDは、試した20のサーバーのうち*平文*の `MSSP-REQUEST` 形式にも +応答した3つだけのサーバーの一つです — telnetオプションより古く、今でもときおり見かける変種です。 + +そのMSSPレポートは当サイトが実測したなかで最大で、**47フィールド**あり、9つの別々のポートについて +`PORT` が9回別々に報告されています。これは壊れた形式ではありません。MSSPの変数はリストであり、多値の +`PORT` を一つの文字列に潰してしまうクローラーは、`"80" "23" "4201"` から整数 `80234201` を作り出します +— これは当プロジェクトが世に出し、そして直したバグであり、ここのパーサーが値を終始リストのまま保つ +理由でもあります。 diff --git a/content/reference/ja/codebase-dikumud.md b/content/reference/ja/codebase-dikumud.md new file mode 100644 index 0000000..606251d --- /dev/null +++ b/content/reference/ja/codebase-dikumud.md @@ -0,0 +1,35 @@ +--- +kind: codebase +slug: dikumud +title: DikuMUD +summary: 戦闘型MUDファミリーの根。レベル、クラス、装備、エリアファイル — そして一世代分の派生を形づくったライセンス。 +codebase: DikuMUD +home: https://dikumud.com/ +see-also: codebases/circlemud +see-also: codebases/rom +see-also: codebases/smaug +see-also: mush-mud-muck-moo +--- + +DikuMUDはコペンハーゲン大学のDatalogisk Institutで書かれ、1991年に公開されたもので、人が何の限定もなく +「MUD」と言うときに指すもののほとんどの祖先です。レベル、キャラクタークラス、ヒットポイント、mob、 +装備スロット、ビルダーがオフラインで書くエリアファイル形式 — 語彙のすべてがここから来ており、Dikuの +ソースを一度も見たことのないゲームでも、その形を受け継いでいます。 + +そのライセンスも話の一部です。Dikuは無償で使えましたが、アクセスに課金することを禁じ、元のクレジットを +表示するよう求めました。この条項こそ、そこから何度もフォークを重ねたゲームのログイン画面に「Dikuの +クレジット」が現れる理由です。 + +直系の子孫 — **Merc**、続いて**ROM**、**CircleMUD**、**SMAUG**、**tbaMUD**、そのほか何十も — は、 +これまで存在したあらゆるMUDの一覧の大きな割合を占めています。 + +## 外から見たときの姿 + +Dikuのファミリーは**MSSP**のファミリーです。MUSH側がログイン画面の `WHO` を通じて接続数を公開し、MSSPは +まったく提供しないのに対して、Diku系のサーバーは圧倒的にtelnetオプション70へ構造化されたレポートで +応答します。ここに載る数値は、そこから来ています。 + +**MCCP2** — ストリームの圧縮 — もこのファミリーではよくあるもので、これをネゴシエートしながら +ストリームを展開できないクライアントは、接続画面をまるごとバイナリのノイズとして受け取ることになる、と +知っておく価値があります。それは当プロジェクト自身のtelnetライブラリにあった実在の不具合で、すでに +修正されています。[MCCP](/reference/protocols/mccp)を参照してください。 diff --git a/content/reference/ja/codebase-evennia.md b/content/reference/ja/codebase-evennia.md new file mode 100644 index 0000000..6daea85 --- /dev/null +++ b/content/reference/ja/codebase-evennia.md @@ -0,0 +1,33 @@ +--- +kind: codebase +slug: evennia +title: Evennia +summary: 完成したゲームではなく、Pythonのフレームワーク。2つのEvenniaのゲームに共通するものが配管だけ、ということもありえます。 +codebase: Evennia +home: https://www.evennia.com/ +see-also: codebases/aresmush +see-also: collaborative-roleplay +see-also: protocols/gmcp +--- + +Evenniaはゲームではなく**MU\*のフレームワーク**です — それがこれについて最初に知るべきことであり、 +Evenniaのゲーム同士を比べることを無意味にしている当のものでもあります。DjangoとTwistedの上に建てられた +Pythonのライブラリで、アカウント、オブジェクト、部屋、コマンド、永続化の層、そしてネットワークスタックを +与えたうえで、ゲームそのものはあなたが書くことを期待します。 + +その帰結として、「Evenniaで動いている」は、「PennMUSHで動いている」よりもはるかに少ないことしか +ゲームについて教えてくれません。Evennia上には戦闘MUDもあれば、ロールプレイのゲームもあり、両者は語彙を +共有していません。2つのEvenniaのゲームに共通するコマンドが一つもない、ということもありえます。 + +すでにPythonを知っている開発者にとっては、何もないところから動く世界までの最短経路であり、2010年代 +半ば以降の新しいゲームのかなりの部分がここから始まっています。 + +## 外から見たときの姿 + +Evenniaは**MSSP**を提供し、それを通じて接続数を公開します。実測したゲームでは**MCCP2** — 圧縮 — も +ネゴシエートしていました。これはtelnetを真剣に扱ったスタックらしい特徴です。 + +Evenniaはフレームワークなので、あるゲームが何をネゴシエートするかは、部分的にはそのゲームの判断です。 +プロトコルの各ページにある採用状況の数値は、サーバーが実際に当サイトへ提供したものの件数であって、 +フレームワークにできることの件数ではありません。そしてEvenniaでは、その2つは多くの場合よりも +離れています。 diff --git a/content/reference/ja/codebase-fluffos.md b/content/reference/ja/codebase-fluffos.md new file mode 100644 index 0000000..267f776 --- /dev/null +++ b/content/reference/ja/codebase-fluffos.md @@ -0,0 +1,33 @@ +--- +kind: codebase +slug: fluffos +title: FluffOS +summary: メンテナンスが続くMudOSの後継であり、生き残っているLPMudの多くが動いているドライバー。ゲームはCではなくLPCで書かれます。 +codebase: FluffOS +home: https://www.fluffos.info/ +see-also: codebases/dikumud +see-also: mush-mud-muck-moo +--- + +LPMudの伝統は、Dikuとは違うところで世界を分けます。**ドライバー** — オブジェクト指向のインタプリタを +走らせるCのプログラム — があり、そして**mudlib** があります。mudlibはゲームまるごとで、**LPC** で書かれ、 +ドライバーが読み込みます。部屋も戦闘もコマンドもログインの手順も、すべてmudlibのオブジェクトであり、 +ドライバーはそのどれについても知りません。 + +そのためLPMudは、戦闘システムから想像されるよりも精神においてMUSHに近くなります。ゲームはゲームの中に +住む言語で書かれ、ドライバーを共有する2つのLPMudが、それ以外に何も共有していないこともありえます。 + +**MudOS**は長年にわたって支配的なドライバーでした。**FluffOS**はそのメンテナンスの続く続編であり、今日 +稼働しているLPのゲームが乗っている可能性が最も高いものです。よく知られたmudlib — Nightmare、Lima、 +Discworld独自のもの — は、さらに別のプロジェクトです。 + +## 外から見たときの姿 + +実測したFluffOSのゲームでは、MSSPと**MCCP2**がありました。MudOSは、当サイトの調査でMSSPとログイン画面の +`WHO` の*両方*に応答した2つだけのコードベースの一つでしたが、返ってきた `WHO` は数値ではなく +プレイヤーごとの一覧でした。 + +mudlibこそがゲームなので、個々のLPのゲームが何をネゴシエートするかは、ドライバーの判断であると同時に +mudlibの判断でもあります — プロトコルの各ページにある採用状況の数値は、サーバーが実際に当サイトへ +提供したものを数えたものであり、このファミリーについては、他の場合よりコードベースについての信号として +弱くなります。 diff --git a/content/reference/ja/codebase-moo.md b/content/reference/ja/codebase-moo.md new file mode 100644 index 0000000..89396d6 --- /dev/null +++ b/content/reference/ja/codebase-moo.md @@ -0,0 +1,29 @@ +--- +kind: codebase +slug: moo +title: MOO +summary: オブジェクト指向で、すべて内側から編集され、ゲームエンジンであると同時に研究と教育のプラットフォームでもあります。 +codebase: MOO +home: https://www.ipomoea.org/moo/ +see-also: mush-mud-muck-moo +see-also: codebases/muck +--- + +MOO — *MUD, Object-Oriented* — は、「世界が自分自身を編集する」という考えを、この趣味の何よりも遠くまで +押し進めています。最初のサーバーであるLambdaMOOが同梱するのは、小さなCのコアとデータベースです。 +利用者が体験するもののほぼすべては、**MOOの言語で、動いているデータベースの中で、それを使っている人々の +手によって**書かれます。部屋に対応するソースファイルは存在しません。 + +その性質は、MOOにゲームの外での生を与えました。90年代を通じて、教育や会議、研究に使われ — +Diversity University、BioMOO、Jay's House — MOOについての技術文献は、この領域のコードベースにしては +不釣り合いなほど学術的です。 + +今日の稼働数は小さいものの、確かにゼロではなく、残っているサーバーは何十年も連続して動き続けていることが +よくあります。 + +## 外から見たときの姿 + +MSSPはなく、実測したゲームでは当サイトが解釈できる `WHO` もありませんでした。代わりにあったのは、 +接続画面の中の *"one of three players are active"* という一文です — このクローラーに綴り字で書かれた +数値を読む処理があるのは、これが理由です。数字だけを見るパーサーはそこに接続数をまったく見出さず、 +そのゲームを永遠に不明として報告し続けたことでしょう。 diff --git a/content/reference/ja/codebase-muck.md b/content/reference/ja/codebase-muck.md new file mode 100644 index 0000000..3293c1b --- /dev/null +++ b/content/reference/ja/codebase-muck.md @@ -0,0 +1,29 @@ +--- +kind: codebase +slug: muck +title: MUCK +summary: Forth風の独自のゲーム内言語を持つTinyMUDの子孫。MUSH側とは異なる社交の文化を持っています。 +codebase: MUCK +home: https://www.fuzzball.org/ +see-also: mush-mud-muck-moo +see-also: codebases/tinymush +see-also: codebases/moo +--- + +MUCK — 実際にはほぼ常に**Fuzzball MUCK**のこと — は、MUSHの系統の子孫ではなくその兄弟です。どちらも +TinyMUDから来ており、どちらもゲームの中にプログラミング言語を置いています。 + +目に見える違いはその言語です。MUF(*Multi-User Forth*)はスタック指向で、MUSHのsoftcodeとはまるで読み味が +違います。一方に堪能なビルダーも、他方では初心者です。その上には、MUSHならsoftcodeがやることに使われる、 +より小さなインラインの式言語MPIが載っています。 + +文化の面では、MUCKはこの趣味の社交とファンダムの世界の大きな部分の本拠地です。そうしたゲームは、 +始まりと終わりのあるシーンではなく、その場にいることと会話を中心に組み立てられる傾向があり、これは +ロールプレイMUSHの伝統との実際の違いであって、テーマの問題ではありません。 + +## 外から見たときの姿 + +MSSPはありません。ログイン前の `WHO` があり、数値で答えます。実測したゲームではtelnetオプションは +ネゴシエートされませんでした — そして調査から、覚えておく価値のある細部が一つあります。その `WHO` の +返答は末尾が空白で終わり、改行がありませんでした。素朴なパーサーが何も報告しなくなるのは、こういうものが +原因です。 diff --git a/content/reference/ja/codebase-pennmush.md b/content/reference/ja/codebase-pennmush.md new file mode 100644 index 0000000..d494a30 --- /dev/null +++ b/content/reference/ja/codebase-pennmush.md @@ -0,0 +1,47 @@ +--- +kind: codebase +slug: pennmush +title: PennMUSH +summary: 最も広く使われているMUSHサーバー。softcode、長いリリースの歴史、そして当サイトの調査でMSSPとログイン前のWHOの両方に応答した2つだけのコードベースの一つ。 +codebase: PennMUSH +home: https://www.pennmush.org/ +see-also: codebases/tinymux +see-also: codebases/rhostmush +see-also: codebases/cobramush +see-also: mush-mud-muck-moo +see-also: protocols/mssp +--- + +PennMUSHは1991年のフォークを経てTinyMUSHから受け継がれたもので、長く続いているロールプレイMUSHの多くが +動かしているサーバーです。その決定的な特徴は**softcode**です。関数型の式言語で、しかるべきビットが +立っている人なら誰でもゲームの内側から編集でき、あるMUSHの振る舞いの大きな部分がそれで書かれています。 +PennMUSHのゲームは、設定されるというより、プレイヤーたちによってプログラムされるものです。 + +バージョンは `1.8.8p0` のように読めます — メジャー、マイナー、そしてパッチレベル — そしてパッチレベルは +よく動きます。パッチレベルがいくつか遅れたバージョンでゲームが動いていることはよくあり、特筆すべき +ことではありません。 + +## 外から見たときの姿 + +PennMUSHは、当サイト自身の38サーバーの調査で、探査する経路の*両方*に応答した2つだけのコードベースの +一つです。求めればMSSPを提供し、ログイン画面で打ち込まれた `WHO` にも答えます。そして実測したゲームでは +その2つが一致していました — これは聞こえるより珍しいことで、そのおかげでPennMUSHは、他のサーバーを +試すときの対照になりました。 + +ログイン前の `WHO` は、便利さを超えた意味を持ちます。MUSHのファミリーがそもそも接続数を公開する手立てが +それだからです — このファミリーの残りの大半は、MSSPをまったく提供しません。この分断こそ、当サイトが +1層ではなく4層を探査する理由であり、それについては[MSSP](/reference/protocols/mssp)を参照してください。 + +現代のPennMUSHではCHARSETのネゴシエーションが普通であり、だからアクセント付きの名前も道中を無事に +生き延びます。 + +## 関連するサーバー + +PennMUSH、**TinyMUX**、**RhostMUSH**、**CobraMUSH**は、共通の祖先と共有された語彙を持つ4つのサーバー +です — 一つを知っているビルダーなら、努力すれば別のもののsoftcodeも読めます。互換性はありません。 +データベースは変換なしにこれらの間を移動しませんし、関数ライブラリも意味のある形で異なります。 + +## SharpMUSH + +PennMUSH互換を目指す.NETによる再実装が、当サイトと同じ作者によって開発中です。このページのどの内容も +そこから実測されたものではなく、カタログにそのゲームはありません。 diff --git a/content/reference/ja/codebase-rhostmush.md b/content/reference/ja/codebase-rhostmush.md new file mode 100644 index 0000000..2f5a7f1 --- /dev/null +++ b/content/reference/ja/codebase-rhostmush.md @@ -0,0 +1,27 @@ +--- +kind: codebase +slug: rhostmush +title: RhostMUSH +summary: 深い権限モデルと大きな組み込み関数群で知られるMUSHサーバー。MSSPはなく、ログイン前のWHOに応答します。 +codebase: RhostMUSH +home: https://github.com/RhostMUSH/trunk +see-also: codebases/pennmush +see-also: codebases/tinymux +see-also: codebases/cobramush +--- + +RhostMUSHは、TinyMUSHから派生した広く使われているサーバーの4番目であり、管理のモデルが最も精緻なもの +です。権限とフラグのシステムは親戚たちよりかなり細かい粒度を持っており、ゲームがこれを選ぶ理由は +たいていそこにあります。 + +組み込みの関数ライブラリは大きく、Rhost向けに書かれたsoftcodeは、他のサーバーにはない関数を使った部分を +書き直さないかぎり、PennMUSHやTinyMUXへきれいには移植できないことがよくあります。 + +## 外から見たときの姿 + +MSSPはありません。ログイン前の `WHO` があり、数値で答えます。CHARSETはネゴシエートされます。 + +この組み合わせ — MSSPはなく、`WHO` は機能する — はMUSHファミリーの署名であり、当サイトがそもそも +ログイン画面を探査する理由です。当サイト自身の調査が示す限り、MSSPのファミリーと `WHO` のファミリーは +ほぼ交わりません。28のコードベースがMSSPを通じて接続数を公開し、7つが `WHO` を通じて公開し、 +両方を通じて公開するのは2つだけです。 diff --git a/content/reference/ja/codebase-rom.md b/content/reference/ja/codebase-rom.md new file mode 100644 index 0000000..a063c48 --- /dev/null +++ b/content/reference/ja/codebase-rom.md @@ -0,0 +1,28 @@ +--- +kind: codebase +slug: rom +title: ROM +summary: Mercの最もよく知られた子孫であり、90年代のMUDの大きな割合がその上に建てられた戦闘エンジン。 +codebase: ROM +see-also: codebases/dikumud +see-also: codebases/smaug +see-also: protocols/mccp +--- + +ROM — *Rivers of MUD* — は**Merc**の派生で、そのMerc自体もDikuMUDの派生ですが、定着したのはこちらでした。 +その戦闘モデル、スキルと呪文のシステム、そしてエリアの形式は、90年代とその後の膨大な数のゲームの +出発点となり、とりわけROM 2.4は、この趣味で最も多くフォークされたソースの一つです。 + +Dikuの系統の他と同じく元のクレジットを表示する要件を引き継いでいるので、系統をほかの方法では特定 +できないゲームでも、ログイン画面にDiku、Merc、ROMの名を挙げていることがよくあります。 + +## 外から見たときの姿 + +実測したゲームでは、MSSP、CHARSET、そして**MCCP2**がありました。 + +ROMは、当プロジェクトが自分自身の圧縮のバグを証明した相手のサーバーです。当サイトの探査はMCCP2を +ネゴシエートし、サーバーは正しく圧縮を始めましたが、依存しているtelnetライブラリはストリームを一度も +展開しませんでした — その結果、接続画面は置換文字の壁として届き、当サイトは短いあいだそれをゲームの側の +落ち度として記録していました。ペイロードは素のzlibの呼び出しできれいに展開でき、それによって話は +曖昧でなくなりました。修正は上流で行われています。顛末は[MCCP](/reference/protocols/mccp)のページに +あります。外から見ると壊れたゲームとまったく同じに見える不具合の、よい例だからです。 diff --git a/content/reference/ja/codebase-smaug.md b/content/reference/ja/codebase-smaug.md new file mode 100644 index 0000000..f062508 --- /dev/null +++ b/content/reference/ja/codebase-smaug.md @@ -0,0 +1,27 @@ +--- +kind: codebase +slug: smaug +title: SMAUG +summary: 大きなテーマ世界のために作られたMercの派生。神々と次元界、そして親の数倍の規模のエリア群を備えています。 +codebase: SMAUG +home: https://www.smaug.org/ +see-also: codebases/dikumud +see-also: codebases/rom +--- + +SMAUG — *Simulated Medieval Adventure multi-User Game* — はRealms of Despairから生まれた、簡素化では +なく肥大の方向に進んだMercの派生です。ROMがMercの戦闘を引き締めたのに対し、SMAUGは足しました。神々、 +次元界、より手の込んだエリアとmobのプログラム、はるかに大きな標準の世界、そしてそれに見合って大きな +ソースツリーを。 + +これを動かしているゲームは大きなテーマものであることが多く、いくつかは25年にわたって連続して +動き続けています。 + +## 外から見たときの姿 + +MSSPがあり、要求すれば応答します。実測したゲームでは、それ以外は何もネゴシエートされませんでした。 + +接続数を比べる人への注意です。当サイトの調査でSMAUGのサーバーはMSSPを通じて報告し、ログイン画面の +`WHO` には答えませんでした。したがってここの数値はゲーム自身のMSSPの `PLAYERS` です — サーバーが行う +主張であり、独立に実測できなかったからこそ額面どおりに受け取っているものです。その数値が2つのどちらから +来たのかは、ゲームのページに表示されます。 diff --git a/content/reference/ja/codebase-tbamud.md b/content/reference/ja/codebase-tbamud.md new file mode 100644 index 0000000..4db660f --- /dev/null +++ b/content/reference/ja/codebase-tbamud.md @@ -0,0 +1,24 @@ +--- +kind: codebase +slug: tbamud +title: tbaMUD +summary: メンテナンスが続くCircleMUDの後継であり、「今日Diku系のMUDを始めたい」への通常の答え。 +codebase: tbaMUD +home: https://tbamud.com/ +see-also: codebases/circlemud +see-also: codebases/dikumud +--- + +tbaMUD — *The Builder Academy MUD* — は、CircleMUD自身の開発が止まったところから引き継ぎ、以来それを +ビルドの通る、パッチの当たった、文書化された状態に保ってきました。自前のフォークではなく既知の良い土台 +からレベルと戦闘のMUDを始めるなら、その土台がこれです。 + +Circleのコードベースの美点をそのまま引き継いでいます。読めるC、文書化されたエリアファイル形式、そして +読者がこれを初めてやると想定したビルダー向けのマニュアルです。The Builder Academy自体も、人々がその上で +建てることを学ぶ教育用のゲームとして稼働しています。 + +## 外から見たときの姿 + +MSSPがあり、要求すれば応答します。探査したゲームでは実測のゼロでした — 当サイトではこれは空欄ではなく +埋まったセルです。接続でき、数値を読み取り、その数値がゼロだった、ということです。これは接続数を +数えられなかったゲームとは別の事実であり、ここでこの2つが一緒にされることはありません。 diff --git a/content/reference/ja/codebase-tinymush.md b/content/reference/ja/codebase-tinymush.md new file mode 100644 index 0000000..dcc1834 --- /dev/null +++ b/content/reference/ja/codebase-tinymush.md @@ -0,0 +1,38 @@ +--- +kind: codebase +slug: tinymush +title: TinyMUSH +summary: MUSHの系統の祖先であり、今もゲームを動かしています。自分のネゴシエーションのバイトが次に送るコマンドを壊しうることを、このクローラーに教えた相手です。 +codebase: TinyMUSH +home: https://github.com/TinyMUSH/TinyMUSH +see-also: codebases/pennmush +see-also: codebases/tinymux +see-also: mush-mud-muck-moo +--- + +TinyMUSHは、PennMUSH、TinyMUX、RhostMUSH、CobraMUSHという系統のすべてが降りてきた場所であり、今も +稼働しています。開発は絶えたのではなく、静かなのです。 + +## 外から見たときの姿 + +MSSPはありません。ログイン前の `WHO` があり、`0 Players logged in, 22 record, no maximum.` という形の +一文で答えます。 + +## このゲームが当サイトに見つけたバグ + +TinyMUSHにここで一段落を割く価値があるのは、当サイト自身のクローラーの不具合を露わにしたゲームだから +であり、その訂正は「実測」が何を意味するはずなのかのよい例になっているからです。 + +当サイトの探査は、何週間ものあいだTinyMUSHを*接続数不明*として読んでいました。記録にあった推測は、 +その返答に末尾の改行がないというものでした。改行はあります。回線から取ったキャプチャによれば、本当の +原因は当サイトの側にありました。**TinyMUSHはログイン画面でtelnetを解釈しません**。そのため接続時に送る +`IAC DO MSSP` の3バイトが、誰かが打ち込んだかのようにその入力バッファへ落ちます。次に読まれる行は +`WHO` ではなく、3つの制御バイトに続く `WHO` であり、それはこのサーバーが持たないコマンドです — だから +接続画面を出し直し、プレイヤーについては何も言いません。 + +現在、探査はネゴシエーションのあとに素の改行を1つ送り、それが生んだものは何であれ捨てます。その出力は +*当サイト*が送ることを選んだバイトへの反応であり、したがってゲームの接続画面でもその答えでもないから +です。TinyMUSHはいま正しく読めており、探査は3分の1の時間で終わるようになりました。 + +確かめなかったディレクトリなら、「このゲームはプレイヤー数を報告しない」と、そのゲームが存在するかぎり +公開し続けたことでしょう。そしてその一文が語っていたのは、当サイトのことだったはずです。 diff --git a/content/reference/ja/codebase-tinymux.md b/content/reference/ja/codebase-tinymux.md new file mode 100644 index 0000000..447a09a --- /dev/null +++ b/content/reference/ja/codebase-tinymux.md @@ -0,0 +1,35 @@ +--- +kind: codebase +slug: tinymux +title: TinyMUX +summary: もう一つの大きなMUSHサーバー。PennMUSHのものと議論になるほど近いsoftcode、MSSPはまったくなし、そして機能するログイン前のWHO。 +codebase: TinyMUX +home: https://www.tinymux.org/ +see-also: codebases/pennmush +see-also: codebases/tinymush +see-also: codebases/rhostmush +see-also: mush-mud-muck-moo +--- + +TinyMUXは、定着したロールプレイMUSHの多くが動かしている2つのサーバーのうちの2番目で、多くのプレイヤーに +とって、これとPennMUSHのどちらを選ぶかは、そのゲームのスタッフがどちらを先に覚えたかの問題です。 +バージョンは `2.12` のような形で読めます。 + +PennMUSHと同じくTinyMUSHの子孫であり、そのsoftcodeは、両者を行き来するビルダーが学び直すのではなく +翻訳をしていると言えるほど近いものです。違いは実在します — 関数ライブラリ、いくつかの解釈の細部、 +`@` コマンド群 — そしてそれこそ、データベースを両者の間で移すことをエクスポートではなくプロジェクトに +してしまうたぐいのものです。 + +## 外から見たときの姿 + +**MSSPはありません。** TinyMUXはこのオプションをまったく提供せず、そのためAresMUSH、MUCK、RhostMUSH、 +CobraMUSH、TinyMUSHとともに、MSSPだけを見るディレクトリにはそもそも見えない、この趣味の側に立っています。 +その接続数はログイン画面の `WHO` から来ており、素の数値で答えます。 + +CHARSETはネゴシエートします。非ASCIIのテキストで親戚の大半より優位に立つのは、そのおかげです。 + +## 接続数の出どころ + +TinyMUXのゲームについて当サイトの数値を別のディレクトリのものと比べるなら、当サイトはログイン画面の +`WHO` を読んでおり、たいていのクローラーはそれを読まない、という点に注意してください。MSSPだけを土台に +したディレクトリは、こうしたゲームを接続数がまったくないものとして報告するか、そもそも一覧に載せません。 diff --git a/content/reference/ja/orientation-collaborative-roleplay.md b/content/reference/ja/orientation-collaborative-roleplay.md new file mode 100644 index 0000000..fab9218 --- /dev/null +++ b/content/reference/ja/orientation-collaborative-roleplay.md @@ -0,0 +1,66 @@ +--- +kind: orientation +slug: collaborative-roleplay +title: 共同ロールプレイがしたい +summary: 求めているのが他の人と一緒にシーンを書くことなら、これがその近道です — 探すべきコードベース、邪魔をしないクライアント、そして最初の晩に何が起きるか。 +see-also: mush-mud-muck-moo +see-also: connecting +see-also: codebases/pennmush +see-also: codebases/aresmush +see-also: codebases/evennia +see-also: clients/mudlet +--- + +ここでいう*共同ロールプレイ*とは、あなたと他の人たちが、キャラクターとして、文章で、たいていは +リアルタイムに、1つのシーンを一緒に書くことです。ゲームがそうしろと言わない限り、誰もイニシアチブを +振ったりしません。これは「MUD」という言葉が覆い隠しているホビーの一面であり、今も生きています。 + +## 探すべきコードベース + +この文化が生きているのはTinyMUDの系統です。そのサーバーが、何かを倒す人のためではなく、何かを作る人の +ために作られているからです。 + +- **[PennMUSH](/reference/codebases/pennmush)**と**[TinyMUX](/reference/codebases/tinymux)** — + 2大MUSHサーバーです。名前を聞いたことのある長寿のロールプレイゲームは、たいていどちらかで + 動いています。 +- **[AresMUSH](/reference/codebases/aresmush)** — 現代的で、ロールプレイのために作られており、 + ウェブのフロントエンドとシーンの記録を、誰かが書いたソフトコードではなく一級の機能として備えて + います。 +- **[Evennia](/reference/codebases/evennia)** — 完成したゲームではなく、Pythonのフレームワークです。 + これを土台にしたゲームは実に多様で、比較的新しいロールプレイゲームのいくつかはEvenniaです。 +- **[MUCK](/reference/codebases/muck)** — 独自の文化を持ち、きわめて社交的で、ファンダムの世界を + 探しているなら知っておく価値があります。 + +目録はこのいずれでも絞り込めます。各コードベースのページから、それを動かしていると当サイトが実測した +ゲームへリンクしています。 + +## 邪魔をしないクライアント + +必ずしも必要ではありません — ここにあるゲームはどれも素のtelnetを話しますし、お使いのOSにはおそらく +すでにクライアントが入っています。ただしシーンを書くうえでは、他の用途よりも重みを増すものが3つ +あります。 + +1. **長い段落に耐える入力行。** 何文にもわたるポーズを打つことになります。 +2. **ログ。** 後からそのシーンを読み返したくなります。 +3. **UTF-8。** 名前にはアクセント記号が入ります。 + +[クライアント](/reference)のセクションをご覧ください。スクリーンリーダーをお使いなら、そこの機能表には +そのための行があります — すべてのクライアントについて答えを確かめられたわけではなく、確かめられな +かったところでは、表は推測をせずに*不明*と記しています。 + +## 最初の晩は実際にどんなものか + +ロールプレイゲームの多くには**申請の手続き**があります。ゲストとして接続し、ヘルプファイルをいくつか +読み、スタッフに読んでもらうためのキャラクターを書き起こします。これには1日かかることも、2週間かかる +こともあります。締め出すこと自体が目的ではなく — ゲームが共有の舞台設定の筋を通すための仕組みです — +とはいえ「ログインして遊ぶ」という形になることは、めったにありません。 + +見た瞬間に誰も接続していないゲームは、ここではごく普通のことです。各ゲームのページにある活動の +ヒートマップのほうが、今の接続数よりも役に立ちます。毎晩15人いて午前4時には誰もいないゲームは、健全な +ゲームを間違った時間に見ているだけです。 + +## 当サイトに分からないこと + +そこにいる人たちが良い人かどうか。スタッフが公正かどうか。その舞台設定を楽しめるかどうか。当サイトが +実測するのはサーバーです。ここに評価はありませんし、これから付くこともありません — それは意図した +決定であって、欠落ではありません。 diff --git a/content/reference/ja/orientation-connecting.md b/content/reference/ja/orientation-connecting.md new file mode 100644 index 0000000..9791f6e --- /dev/null +++ b/content/reference/ja/orientation-connecting.md @@ -0,0 +1,46 @@ +--- +kind: orientation +slug: connecting +title: 接続のしかた +summary: ホストとポート、そしてtelnet。ゲームのページに載っているアドレスが何を意味し、それをどうするか。 +see-also: mush-mud-muck-moo +see-also: protocols/tls +see-also: protocols/charset +--- + +ここに掲載しているゲームはどれも**ホストとポート**で応答し、その下にあるプロトコルはtelnetです — +実際のところ、生のTCP接続の上に、任意のネゴシエーションが少しだけ乗ったものです。 + + telnet mush.pennmush.org 4201 + +これで通じますし、多くのシステムにはすでに入っています。ただし遊び方としては貧弱です。システムの +`telnet`には、名に値するローカルエコーの制御も、ログも、履歴もなく、ASCIIを超えるものはすべて壊して +しまいます。ゲームが応答するかどうかを確かめるには適した道具ですが、一晩を過ごすには向いていません。 + +## ゲームのページのアドレスが伝えること + +各ゲームのページには、当サイトが実測したエンドポイントを載せ、**TLS**を観測したものには印を付けて +います。TLSのポートがあるゲームには、暗号化して接続できます。そのポート番号は、たいてい平文のものとは +別です。 + +ゲームに複数のポートがある場合、それは別々のゲームではなく、同じ世界へ別の道から到達していることが +多いものです。当サイトは実測したものを並べるだけで、どれが正式なものかを推測することはしません。 + +## クライアントを選ぶ + +[クライアント](/reference)のセクションには、機能表の付いたページが1つずつあります。何かを入れる前に +確かめておく価値があるのは、次の3つです。 + +- **UTF-8を扱えるか。** ゲームが英語だけのものでないなら、これは最初の晩に問題になります。 +- **TLSを扱えるか。** ゲームが提供している場合にしか関係しませんが、今では提供しているものが + いくつもあります。 +- **スクリーンリーダーを使うなら、そのプロジェクトは対応を文書化しているか。** これはクライアントの + 比較で最も抜け落ちやすい行なので、当サイトでは最初の行に置いています — そして誰も答えを確かめて + いないところでは、*不明*と記します。 + +## 何も応答しないとき + +応答しないゲームが、必ずしもなくなったわけではありません。ゲームはホストを移りますし、DNSは失効します +し、ファイアウォールには言い分があります。当サイトは、これまでに実測したすべてのゲームを — 何年も前に +応答しなくなったものも含めて — 残しており、毎週ノックし続けています。ですから何かを結論づける前に見る +場所は[アーカイブ](/archive)です。 diff --git a/content/reference/ja/orientation-families.md b/content/reference/ja/orientation-families.md new file mode 100644 index 0000000..df6e4c5 --- /dev/null +++ b/content/reference/ja/orientation-families.md @@ -0,0 +1,81 @@ +--- +kind: orientation +slug: mush-mud-muck-moo +title: MUSH、MUD、MUCK、MOO — 言葉の意味するもの +summary: 4つの伝統を指す4つの言葉であり、そのどれもジャンルではありません。これらが実際に伝えていること。 +see-also: collaborative-roleplay +see-also: connecting +see-also: codebases/pennmush +see-also: codebases/aresmush +see-also: codebases/muck +see-also: codebases/moo +see-also: codebases/evennia +--- + +これらの言葉はどれも**サーバーソフトウェアの系統**を指す名前であって、ゲームの種類を指すものでは +ありません。これらについて知っておけることの中で、これがずば抜けて役に立ちます。「これはMUSHですか、 +MUDですか」という問いにろくな答えが返らないことが多いのも、そのためです。正直な答えはたいてい +*どちらでもあり、あなたが本当に訊きたかったのは文化のことだ*、というものです。 + +## MUD + +最も古い言葉であり、今では最も広い言葉です。*Multi-User Dungeon* — バートルとトラブショーによる1978年 +のゲーム — として始まり、90年代半ばには、テキストによるあらゆる多人数世界を指す総称になっていました。 + +狭い意味で使われるときは、**DikuMUDとLPMudの系統**を指します。レベル、戦闘、装備、そしてビルダーが +あらかじめ書いた部屋を記述するエリアファイルを中心に組み立てられたサーバーです。誰かが「MUDを遊んで +いる」と言い、それが何か具体的なものを指しているなら、たいていはこれです。 + +一覧では、それぞれの系統を単独で見られます。[DikuMUDのゲーム](/games?lineage=DikuMUD)と +[LPMudのゲーム](/games?lineage=LPMud)です。 + +## MUSH + +TinyMUDの系統から出た*Multi-User Shared Hallucination*です。この語を決めているのはテーマではなく +**ソフトコード**です。MUSHのサーバーは、プレイヤーがゲームの内側から使うプログラミング言語を備えて +いて、ビルド権限を持つプレイヤーは、ソースファイルに触れることも、何かを再起動することもなく、部屋や +オブジェクトや振る舞いを作れます。 + +この1つの設計判断が文化を生みました。MUSHは自動化された仕組みが薄く、人の手による仕組みが濃くなり +がちです — スタッフが回すプロット、書かれたシーン、申請の手続き — 遊んでいる人が、そのまま作っている +人でもあるからです。 + +PennMUSH、TinyMUSH、TinyMUX、RhostMUSH、CobraMUSH、AresMUSHはいずれもこの系統に属していますが、そう +名乗るものは1つもありません。MSSPには公開できる`MUSH`という値がなく、PennMUSH以外はそもそもMSSPを +まったく公開しないからです。したがって、これらをまとめるのは当サイトが読み取ったことではなく、当サイト +が行っていることです。[MUSHのゲーム](/games?lineage=MUSH)が、現れる場所すべてで*導出*と記されているのは +そのためです。 + +## MUCK + +MUSHと同じくTinyMUDの子孫で、独自のソフトコード(Forth系の言語であるMUF)を持ち、社交的な世界と +ファーリーファンダムの世界という強い伝統があります。技術的にはMUSHに近く、文化的には、両方を遊ぶ人が +同じものだとは言わない程度に異なります — [MUCKのゲーム](/games?lineage=MUCK)。 + +## MOO + +*MUD, Object-Oriented*です。「ゲームが自分自身を編集する」という発想の最も純粋な形で、MOOの中身は +ほぼすべて、それを使う人たちが内側から、MOOのプログラミング言語で書いたものです。LambdaMOOがその祖先 +であり、MOOは歴史的に、ゲームと同じくらい教育と研究の場でも親しまれてきました。 +[MOOのゲーム](/games?lineage=MOO)。 + +## では、実際には何を訊けばよいのか + +4文字の言葉よりも、次の3つの問いのほうがよほど役に立ちます。 + +1. **戦闘はあるか、そしてそれは自動化されているか。** これはどの名前よりも確実に、Diku/LPの系統と + TinyMUDの系統を分けます。 +2. **誰が作るのか。** スタッフだけか、それともビルドのビットを持つ人なら誰でもか。 +3. **遊びは予定されたものか、それとも流れているものか。** 約束して行うシーンとポーズによる + ロールプレイか、それともログインしてすぐ動き出すのか。 + +当サイトの一覧は、最初の問いの一部には答えられます。あるゲームについて当サイトが実測した +**コードベース**は、そのサーバーがどの伝統から来ているかを教えてくれますし、**系統**のファセットは、 +その答えを絞り込みに使えるようにしたものです。文化までは分かりませんし、このページはそのふりを +しません。 + +そのファセットについて、注意を1つ。このページこそ、それに初めて出会う場所だからです。コードベースは +実測ですが、系統は実測ではありません。それはゲームが伝えてきた内容についての*当サイトの*まとめで +あり、*実測*や*自己申告*と並ぶ独自の札 — **導出** — の下に置かれています。親が争いなく定まらない +コードベースは、最も近いものに押し込むのではなく、どの系統からも外します。そうしたゲームのいくつかは +`FAMILY Custom`を公開して、自らの言葉で当サイトと同じことを言っています。 diff --git a/content/reference/ja/protocol-atcp.md b/content/reference/ja/protocol-atcp.md new file mode 100644 index 0000000..4198145 --- /dev/null +++ b/content/reference/ja/protocol-atcp.md @@ -0,0 +1,30 @@ +--- +kind: protocol +slug: atcp +title: ATCP +summary: GMCPの前身。ペイロードの形式がより緩い帯域外データで、大部分は置き換えられましたが、削除しなかったサーバーでは今もネゴシエートされています。 +protocol: ATCP +see-also: protocols/gmcp +see-also: protocols/msdp +see-also: clients/mudlet +--- + +ATCP — Achaea Telnet Client Protocol — はtelnetオプション200であり、MUDのテキストと並べて構造化 +データを送るという発想が初めて広く実装された場所です。サーバーがモジュール名とペイロードを送り、 +クライアントがそれを振り分けます。 + +ペイロードの形式は[GMCP](/reference/protocols/gmcp)のJSONよりも緩く、GMCPが取って代わったのは基本的に +そのためです。ATCPに対応するクライアントも、今では非推奨として記載し、代わりにGMCPを案内するのが +普通です。 + +## 今も残っている理由 + +有効にしたままでも何も壊れないからです。2008年にATCPを実装し、2014年にGMCPを追加したサーバーは、 +たいてい今も両方をネゴシエートしますし、両方に対応するクライアントは提供されたほうを使います。 + +新しく実装するなら、これを選ぶ理由はありません。 + +## 当サイトが実測するもの + +当サイトが観測したハンドシェイクでtelnetオプション200を提供したサーバーです。ここの数値が低いのは +想定どおりで、それはこのプロトコルの古さの話であって、それ以外の何かの話ではありません。 diff --git a/content/reference/ja/protocol-charset.md b/content/reference/ja/protocol-charset.md new file mode 100644 index 0000000..2faaec1 --- /dev/null +++ b/content/reference/ja/protocol-charset.md @@ -0,0 +1,40 @@ +--- +kind: protocol +slug: charset +title: CHARSET +summary: エンコーディングを取り決めるための、RFC 2066のtelnetオプション。ゲームのアクセント付きの名前が無事に届く理由であり、これがないときに起きる分かりにくい失敗の原因でもあります。 +protocol: CHARSET +home: https://www.rfc-editor.org/rfc/rfc2066 +see-also: protocols/ttype +see-also: connecting +see-also: codebases/tinymux +--- + +CHARSETはtelnetオプション42で、RFC 2066で規定されています。一方が文字セットの一覧を提示し、もう一方が +そこから1つを選び、両者はバイトと文字の対応について合意します。 + +実際には、このネゴシエーションは**UTF-8**に落ち着くか、そもそも行われないかのどちらかです。MUSHの系統は +MUDの系統よりもはっきり多くネゴシエートしており — TinyMUX、RhostMUSH、PennMUSHはいずれも行います — +これは名前の入った文章を書く人たちの集まりであることの表れです。 + +## これがないと何が起きるか + +クライアントは推測するしかなく、たいていの推測はASCIIかLatin-1です。ASCIIと推測すれば0x7Fを超える +バイトはすべて疑問符になり、UTF-8のサーバーをLatin-1と推測すればアクセント付きの文字はどれも2つの +記号に化けます。どちらの失敗もゲームのせいに見えますが、そうではありません。 + +クローラーにとっては、これが特定の場所で効いてきます。当サイトが使っているtelnetライブラリは、現在の +エンコーディングの既定値をASCIIにしており、この既定値は無害ではありません — CHARSETを一度も +ネゴシエートしないサーバー、つまり大半のサーバーでは、すべてのバイトがこれで復号されます。だからこそ +当サイトは意図してその初期値を与えています。 + +## CHARSETが届かない唯一の場所 + +MSSPのフィールド名と値は、CHARSETが何に落ち着いたかに関係なくASCIIとして復号されます。 +サブネゴシエーションはテキストではなくコマンドであり、仕様がCHARSETの適用範囲をテキストに限って +いるからです。これは仕様に適合していると言えなくもありませんが、情報は失われます。MSSPの`NAME`が +`Café Noir`のゲームは`Caf? Noir`と報告し、元のバイトは当サイトが手を出せるどの段階よりも前に +消えています。 + +当サイトの自己申告のフィールドで文字化けが見えるのに、ゲーム自身の出力では見えない場合、理由はこれで +あり、当サイトの側からは復元できません。 diff --git a/content/reference/ja/protocol-gmcp.md b/content/reference/ja/protocol-gmcp.md new file mode 100644 index 0000000..82541e0 --- /dev/null +++ b/content/reference/ja/protocol-gmcp.md @@ -0,0 +1,43 @@ +--- +kind: protocol +slug: gmcp +title: GMCP +summary: Generic Mud Communication Protocol — テキストと並んで流れる構造化されたJSONメッセージであり、現代のクライアントの多くが土台にしている帯域外チャネルです。 +protocol: GMCP +home: https://www.mudhalla.net/tintin/protocols/gmcp/ +see-also: protocols/msdp +see-also: protocols/atcp +see-also: clients/mudlet +--- + +GMCPはtelnetオプション201です。ネゴシエートが済むと、サーバーは**構造化データを帯域外で**送れる +ようになります。パッケージ名とJSONのペイロードが、テキストと同じストリームで届きますが、テキストの +一部ではありません。 + +`Char.Vitals { "hp": 412, "maxhp": 500 }` が定番の例です。クライアントはこれだけで体力バーを動かせて、 +文章から数値を拾い集める必要がありません。それがこのプロトコルの要点そのものです — テキストの +パターンマッチで作ったステータス表示は、ゲームがプロンプトを変えた日に壊れますが、GMCPで作ったものは +壊れません。 + +パッケージの名前空間は標準化されたものではなく、慣習によるものです。`Char`、`Room`、`Comm`、`Client` +は広く使われていますが、そこから先はゲームが必要なものを考え出すので、あるゲームが何を送ってくるかは、 +たいていクライアントに教えてやる必要があります。 + +## ATCPに取って代わった理由 + +GMCPは[ATCP](/reference/protocols/atcp)の後継です。ATCPは同じ仕事を、より緩いペイロード形式で +こなしていました。改良点はJSONで、移行は2010年代半ばにはおおむね完了していました。両方に対応する +ゲームは珍しくありませんが、ATCPだけに対応する新しいゲームは珍しいでしょう。 + +## 当サイトが実測するもの + +ここに数えられるのは、**当サイトが観測したハンドシェイクで、そのゲームのサーバーがGMCPを提供した** +場合です。これは、ゲームのMSSPが`GMCP 1`と述べていることとは別の主張です。このホビーのプロトコル表の +大半は後者を土台にしていますが、両者はしばしば食い違います。 + +当サイト自身の経緯から、実測についての注記が1つあります。ある時期、[MCCP](/reference/protocols/mccp) +も併せてネゴシエートするサーバーでは、GMCPが見えていませんでした。当サイトのtelnetライブラリが圧縮を +ネゴシエートしながらストリームを展開しておらず、圧縮の開始位置より後はすべて雑音になっていたためです。 +調査したサーバーのうち少なくとも1つは、実は最初からGMCPを話していたことが分かりました。よく知っている +系統についてこのページの数値が低く見えるなら、まず疑うべきはこの種の欠陥です — 相手側ではなく、 +当サイトの側の。 diff --git a/content/reference/ja/protocol-mccp.md b/content/reference/ja/protocol-mccp.md new file mode 100644 index 0000000..104bb7d --- /dev/null +++ b/content/reference/ja/protocol-mccp.md @@ -0,0 +1,41 @@ +--- +kind: protocol +slug: mccp +title: MCCP +summary: ストリームの圧縮。安上がりで広く普及しており、このプロジェクトの歴史で最も学ぶところの多い不具合を生んだプロトコルです。 +protocol: MCCP +home: https://www.mudhalla.net/tintin/protocols/mccp/ +see-also: codebases/rom +see-also: codebases/dikumud +see-also: protocols/gmcp +--- + +MCCPは、サーバーからクライアントへのストリームをzlibで圧縮します。バージョン1はtelnetオプション85で、 +事実上は歴史上のものです。**バージョン2**はオプション86で、現代のサーバーがネゴシエートするのは +こちらです。サーバーが`IAC SB MCCP2 IAC SE`を送った後は、続くバイトはすべて1本の連続したzlib +ストリームの一部になります。 + +テキストのプロトコルでは本当に効果があり — MUDの出力は非常によく圧縮されます — DikuとLPの系統では +よく使われていて、当サイトが調査したコードベースのおよそ3分の1がネゴシエートします。 + +## 失敗の仕方と、それがここで重要な理由 + +MCCP2をネゴシエートしておきながらストリームを展開しないクライアントは、**圧縮の開始位置から先は +バイナリのごみ**を受け取ります。エラーでも切断でもありません。接続画面は置換文字の壁として届き、その +後に来るもの — `WHO`の応答、その後のMSSP、セッションのすべて — は失われます。 + +これは仮定の話ではありません。当サイトのtelnetライブラリがまさにそうしていました。オプションを +ネゴシエートし、「圧縮が有効になった」というコールバックを発火させながら、1バイトも展開して +いませんでした。ペイロードは素のzlib呼び出しできれいに展開できたので、正しいのはサーバーの側で、 +そうでないのは当サイトの側だと疑いようがありませんでした。調査した38のコードベースのうち13が影響を +受け、その間、それらのサーバーが圧縮の開始*より後*に何をネゴシエートしたのかは観測できませんでした +— つまり、当サイトによるそれらの機能の記録は、実際より少なく見せていたことになります。 + +これは上流で修正されました。後続の欠陥 — 展開器を接続の間ずっと保持せず、読み取りのたびに作り直して +いるため、大きな接続画面の途中で失敗するというもの — は報告済みで未解決のままであり、最も大きな画面の +末尾に影響します。 + +読む人に持ち帰ってほしいことが2つあります。**このページのプロトコルの数値は、このホビーの実測で +あると同時に、当サイトのクローラーの実測でもある**ということ。そして、それが誤っていたと分かっている +箇所については、当サイトはそう明記します。もう1つ、クライアントを書いているなら、MCCPをネゴシエート +するのは簡単で、正しく展開するところに手間がかかるということです。 diff --git a/content/reference/ja/protocol-msdp.md b/content/reference/ja/protocol-msdp.md new file mode 100644 index 0000000..fb286ea --- /dev/null +++ b/content/reference/ja/protocol-msdp.md @@ -0,0 +1,30 @@ +--- +kind: protocol +slug: msdp +title: MSDP +summary: Mud Server Data Protocol — GMCPと同じ仕事を、コンパクトなバイナリ符号化と、GMCPにはない探索の仕組みでこなします。 +protocol: MSDP +home: https://www.mudhalla.net/tintin/protocols/msdp/ +see-also: protocols/gmcp +see-also: clients/tintin +see-also: clients/blightmud +--- + +MSDPはtelnetオプション69で、[GMCP](/reference/protocols/gmcp)と同じ問題を解きます。テキストと並べて +構造化データを送り、クライアントが文章から数値を拾い集めずに済むようにする、というものです。 + +違いは2つあります。MSDPの符号化は**バイナリでコンパクト**であり — 変数と値はJSONで包むのではなく、 +1バイトの制御文字で標されます — さらにMSDPは**探索**のやり取りを定めています。クライアントは +`COMMANDS`や`REPORTABLE_VARIABLES`などを`LIST`で問い合わせ、そのゲームが何に対応しているかを教えて +もらえます。GMCPに相当するものはなく、だからGMCPのクライアントは、たいていゲームごとに設定してやる +必要があります。 + +実際には普及ではGMCPが勝ち、MSDPは実装したサーバーとクライアントに、しばしばGMCPと並んで残って +います。 + +## 当サイトが実測するもの + +ここに数えられるのは、当サイトが観測したハンドシェイクで、そのゲームのサーバーがMSDPを提供した場合 +です。このセクションのどの数値とも同じで、これは肯定的な観測であり、残りがその反対だということには +なりません — 数えられていないゲームは、MSDPを実装していないのかもしれませんし、単にまだ当サイトが +そのハンドシェイクを読み取っていないだけかもしれません。 diff --git a/content/reference/ja/protocol-msp.md b/content/reference/ja/protocol-msp.md new file mode 100644 index 0000000..c7221c3 --- /dev/null +++ b/content/reference/ja/protocol-msp.md @@ -0,0 +1,40 @@ +--- +kind: protocol +slug: msp +title: MSP +summary: MUD Sound Protocol — サーバーが音声ファイルを指定し、クライアントがそれを再生します。古く、単純で、他の2つと取り違えやすいプロトコルです。 +protocol: MSP +home: https://www.zuggsoft.com/zmud/msp.htm +see-also: protocols/mxp +see-also: clients/vipmud +--- + +MSPを使うと、サーバーはクライアントに音を鳴らすよう頼めます。ファイル名、音量、繰り返し回数、そして +クライアントが持っていない場合の取得先URLを並べた、括弧で囲まれた指示です。telnetオプション90で +ネゴシエートしますが、何もネゴシエートしないサーバーが、テキストのストリームに帯域内で送ることも +できます。 + +本当に古く、それでいて本当に今も使われています — テキストのゲームにおける環境音は、言葉の響きよりも +大きな効果があり、クライアントの画面表示ではなく音の手がかりを頼りにしているプレイヤーにとっては、 +飾り以上のものです。 + +## これではない3つのもの + +このセクションのクライアント表では、ここで慎重にならざるをえませんでした。その理由は書き留めておく +価値があります。 + +- **MCMP** — Mud Client Media Protocol — は、似た仕事をする別のプロトコルです。MSPではなくMCMPを + 実装しているクライアントが少なくとも1つあり、片方をもう片方として読み取れば、誰もしていない主張を + 表に載せることになります。 +- **クライアント自身の「音を鳴らす」スクリプト呼び出し**はMSPではありません。それはスクリプトの指示で + ローカルのファイルを再生するものです。MSPは、サーバーがクライアントに何を再生するかを伝えるもの + です。 +- **同梱プラグインによる対応は、そのように書くだけの価値があります。** あるクライアントのMSP対応は、 + telnetのネゴシエーションを明示的に行わないプラグインとして同梱されており、MSPを帯域内で送る + サーバーでは動きますが、ネゴシエートすることを前提とするサーバーでは動きません。 + +## 当サイトが実測するもの + +telnetオプション90を提供したサーバーです。MSPはネゴシエーションなしに帯域内で送られることが多いため、 +この数値は実際の普及を、見積もれない分だけ少なく示しています — これはハンドシェイクで見える範囲の +限界であって、このプロトコルについての知見ではありません。 diff --git a/content/reference/ja/protocol-mssp.md b/content/reference/ja/protocol-mssp.md new file mode 100644 index 0000000..e49a2d5 --- /dev/null +++ b/content/reference/ja/protocol-mssp.md @@ -0,0 +1,58 @@ +--- +kind: protocol +slug: mssp +title: MSSP +summary: Mud Server Status Protocol — ゲームがクローラーに自分自身を伝える手段です。そこで報告されるものはすべて実測ではなく自己申告であり、当サイトはこの2つを分けて扱います。 +protocol: MSSP +home: https://www.mudhalla.net/tintin/protocols/mssp/ +see-also: protocols/gmcp +see-also: codebases/dikumud +see-also: codebases/pennmush +--- + +MSSPはtelnetオプション70です。クローラーが`IAC DO MSSP`を送ると、対応しているサーバーは、自分自身を +説明する名前と値の組の表を返します — 名前、接続数、コードベース、稼働時間、ホスト名、ポート、 +ジャンル、そのほか公開したいと考えたものです。 + +このホビーで機械可読なディレクトリ項目に最も近いものであり、いくつかのディレクトリがそもそも存在して +いるのは、これのおかげです。 + +## MSSPレポートの内容はすべて主張である + +当サイトが既存のどのディレクトリとも異なるのは、この点です。MSSPレポートは、ゲームが自分自身について +*語ったもの*です。MSSPの表にある`GMCP 1`が意味するのは、誰かが設定ファイルに`1`と打ち込んだ、それも +おそらく2011年に、ということだけです。サーバーがGMCPを提供している証拠ではありませんし、両者は +興味深いと言える程度にはよく食い違います。 + +そのため、MSSPから得た事実には当サイトで**自己申告**の札を付けます。そして同じ事実を実測できる場合 — +機能であれば、そのオプションが実際にネゴシエートされるかどうかを見ることで実測できます — 両方を、 +それぞれの古さを添えて並べて表示します。MSSPで6年間GMCPを自己申告していながら、ハンドシェイクでは +一度も提供したことのないゲーム。それは知る価値のある事実であり、他のどこにも見つけられません。 + +当サイトが意図してまったく算入しない唯一のフィールドが`CREATED`です。手で打ち込まれた1行にすぎず、 +これを何かに算入すれば、その何かは造作なく操作できるものになってしまいます。 + +## 誰が応じるのか + +MSSPは**DikuとLP**の側の答えです。当サイトが行った38コードベースの調査では、28がMSSPで接続数を公開し、 +7つがログイン画面の`WHO`で公開し、両方を行ったのは2つだけでした — この2つの系統はほぼ完全に重なり +ません。AresMUSH、TinyMUX、MUCK、RhostMUSH、CobraMUSH、TinyMUSHはMSSPをまったく提供しません。 + +1つではなく4つの層を探査することの、これが経験的な根拠です。**MSSPだけを土台にしたクローラーには、 +MUSHの系統の大半が見えません**。それはこのホビーの大きな部分であり、当サイトが想定する読者の大半でも +あります。 + +## 待つのではなく、こちらから訊く + +MSSPに完全に対応しているサーバーの多くは、自分からは決して差し出してきません — `IAC DO MSSP`には +応じますが、それ以外では何も言いません。そのため、`IAC WILL NAWS`で始めて待つだけのクローラーは、 +それらのゲームを何も公開していないものとして報告します。それは、クローラー自身の沈黙からサーバーに +ついて立てた主張です。当サイトは接続時に`IAC DO MSSP`を送ります。 + +## 平文の形式 + +クライアントがログイン画面で`MSSP-REQUEST`という行をそのまま送る、古い変種があります。当サイトは +これを実測しました。試した20のゲームのうち3つが応じ、その3つはいずれもtelnetオプション70にも応じて +いたので、オプションで届く範囲を超えるものには何も届きませんでした。8つのサーバーはこの要求を +**キャラクター名**として読み取り、そう返答して、見知らぬ相手に許されているログイン試行を1回 +消費しました。当サイトはこれを送りません。 diff --git a/content/reference/ja/protocol-mxp.md b/content/reference/ja/protocol-mxp.md new file mode 100644 index 0000000..fa71a97 --- /dev/null +++ b/content/reference/ja/protocol-mxp.md @@ -0,0 +1,39 @@ +--- +kind: protocol +slug: mxp +title: MXP +summary: MUD eXtension Protocol — テキストのストリームに埋め込まれたHTMLに似たマークアップで、クリックできるリンク、画像、フォームをもたらします。仕様は広く行き渡っていますが、実装はまちまちです。 +protocol: MXP +home: https://www.zuggsoft.com/zmud/mxp.htm +see-also: protocols/pueblo +see-also: clients/mushclient +see-also: clients/mudlet +--- + +MXPは、サーバーが送るテキストに、HTMLに似た小さなマークアップ言語を埋め込みます。クリックできる +コマンドのための``、リンクのための``、色とフォントの要素、そしてサーバーが独自のタグを +定義するための仕組みです。telnetオプション91でネゴシエートします。 + +設計上の問題は本質的で、しかも興味深いものです。マークアップはテキストと同じストリームを流れるので、 +サーバーはマークアップに*見える*テキストに注意しなければならず、クライアントは何を描画するかに注意 +しなければなりません。MXPがセキュリティレベルを定めているのは、まさにこのためです — 他のプレイヤーの +発言の行に混ざって届いたタグは、サーバー自身が出力したタグとは別物です。 + +## 求められる理由はクリックできること + +MXPが実際に使われている用途の大半は、`north`やアイテム名を、クリックできるものに変えることです。 +新しいプレイヤーにとってこれは大きな違いであり、複雑さにもかかわらずこのプロトコルが実装され続けて +いる理由でもあります。 + +## もう一方がPueblo + +[Pueblo](/reference/protocols/pueblo)はMXPより古く、似た仕事を、より文字どおりHTMLの形をした別の方法で +こなします。片方に対応しているクライアントがもう片方には対応していないことは多く、機能一覧を読むときに +両者は取り違えやすいものです — このセクションのクライアント表で、当サイトが気をつけなければならな +かった間違いでもあります。 + +## 当サイトが実測するもの + +当サイトが観測したハンドシェイクでtelnetオプション91を提供したサーバーです。MXPは帯域外のプロトコル +ほど頻繁にはネゴシエートされません。その一因は、価値の多くが、まったくネゴシエートせずにマークアップを +ただ送って期待するサーバーによって実現されていることにあります — それは当サイトからは見えません。 diff --git a/content/reference/ja/protocol-pueblo.md b/content/reference/ja/protocol-pueblo.md new file mode 100644 index 0000000..5136856 --- /dev/null +++ b/content/reference/ja/protocol-pueblo.md @@ -0,0 +1,31 @@ +--- +kind: protocol +slug: pueblo +title: Pueblo +summary: 同名のクライアントから生まれた、MUDにHTMLを載せる古いほうの方式。MUSH側のクライアントでは今も対応されており、MXPと取り違えられることがよくあります。 +protocol: PUEBLO +home: https://pueblo.sourceforge.net/ +see-also: protocols/mxp +see-also: clients/beipmu +--- + +Puebloは90年代半ばに同名のクライアントから生まれ、MUDのテキストを豊かにするのに直接的な方法を +取りました。サーバーに**HTML**を送らせ、クライアントにそれを描画させる、というものです。サーバーは +接続時の1行でPuebloへの対応を告げ、クライアントが応答すると、そこから先はストリームがマークアップを +運べるようになります。 + +このホビーではMUD側よりMUSH側に広まり、対応しているMUSHのサーバーは、今もたいてい対応したままです。 + +## MXPではない + +[MXP](/reference/protocols/mxp)は後発の方式で、より広く実装されています。両者は似た仕事をしますが +互換性はなく、クライアントのPuebloへの対応をMXPへの対応として読み取ること — またはその逆 — は、 +クライアントの比較をまとめるときに最も犯しやすい間違いです。このセクションのクライアントのページが +両者を分けているのはそのためであり、あるプロジェクトが片方だけを文書化している場合、もう片方は*不明* +と記します。 + +## 当サイトが実測するもの + +Puebloのハンドシェイクは通常の意味でのtelnetオプションではないため、当サイトが観測できる範囲は、 +ネゴシエートされるプロトコルの場合より狭くなります。ここの数値が低い場合は、普及についての言明では +なく、当サイトから何が見えているかについての言明として読むべきです。 diff --git a/content/reference/ja/protocol-tls.md b/content/reference/ja/protocol-tls.md new file mode 100644 index 0000000..c26e259 --- /dev/null +++ b/content/reference/ja/protocol-tls.md @@ -0,0 +1,36 @@ +--- +kind: protocol +slug: tls +title: TLS +summary: 暗号化された接続。ネゴシエートによる切り替えではなく別のポートであるのが普通で、当サイトが訊くのではなく接続して確かめる唯一の機能です。 +protocol: TLS +see-also: connecting +see-also: protocols/charset +see-also: clients/potato +--- + +telnetは平文です。MU\*に送るものはすべて — パスワードも含めて — ゲームがTLSを提供していない限り、 +経路上のあらゆるものに読める状態でネットワークを渡ります。 + +このホビーでTLSといえば、ほぼ必ず**最初のバイトからTLSを話す2つ目のポート**のことであり、帯域内での +切り替えではありません。4201に平文のポート、4202にTLSのポートというのがよくある形です。ネゴシエート +する変種もありますが、少なくとも1つのクライアントの文書が非対応と明記する程度には珍しいものです。 + +## ゲームのページがこれを特別に示す理由 + +TLSは、当サイトで*実際にやってみる*ことによって確かめている唯一の機能です。あるエンドポイントにTLSの +印が付いているのは、当サイトがそこに対してTLSハンドシェイクを完了したからです。訊くという手順はなく、 +自己申告するフィールドもありません。そのため、この目録の中で最も濁りのない実測になっています。 + +ゲームのTLSのポートと平文のポートをまとめず、別々のエンドポイントとして掲載しているのも、そのため +です。両者は別のものについての別の実測です。 + +## 実際的な助言 + +遊んでいるゲームがTLSのポートを提供しているなら、そちらを使ってください。提供しておらず、それが気に +なるなら、頼んでみてください — 管理者にとっては小さな手間ですし、これが当たり前になっていない理由は、 +誰かが反対しているからというより、たいていは誰も頼まなかったからです。 + +頼りにする前に、使っているクライアントが対応しているかを確かめてください。[クライアント](/reference) +のセクションにあるもののいくつかは対応しています。少なくとも1つは、代わりに外部の`stunnel`プロセスを +使う回避策を文書化していて、それは実際に動きますが、大半の人がやる気になるよりは手間のかかる設定です。 diff --git a/content/reference/ja/protocol-ttype.md b/content/reference/ja/protocol-ttype.md new file mode 100644 index 0000000..f4d2237 --- /dev/null +++ b/content/reference/ja/protocol-ttype.md @@ -0,0 +1,44 @@ +--- +kind: protocol +slug: ttype +title: TTYPEとMTTS +summary: クライアントが、自分は何であり何ができるかをサーバーに伝えるための仕組み — クライアントがそう伝えることを選べば、スクリーンリーダーが使われていることも含めて。 +protocol: TTYPE +home: https://www.mudhalla.net/tintin/protocols/mtts/ +see-also: protocols/charset +see-also: clients/tintin +see-also: clients/blightmud +--- + +TTYPEはRFC 1091によるtelnetオプション24です。サーバーがクライアントに端末の種類を尋ね、クライアントが +答えます。歴史的には、その答えは`VT100`か`ANSI`でした。 + +**MTTS** — Mud Terminal Type Standard — は、その上に慣習を重ねたものです。クライアントは3回答えます。 +自分の名前、端末の種類、そして`MTTS `であり、そのビットが機能を申告します。256色、 +トゥルーカラー、UTF-8、MNES、帯域外でのMSP — そしてとりわけ**`MTTS_SCREEN_READER`**です。 + +## スクリーンリーダーのビット + +最後のものは、立ち止まって見る価値があります。このホビーのプロトコル群の中で、アクセシビリティが一級の +概念になっている唯一の場所だからです。 + +このビットを立てるクライアントは、スクリーンリーダーが使われていることをサーバーに伝えています。それに +気づいたサーバーは、そのように振る舞いを変えられます。ASCIIアートを出さない、部屋の説明を囲む飾り罫を +落とす、表の組み方を変える、といったことです。[TinTin++](/reference/clients/tintin)と +[Blightmud](/reference/clients/blightmud)はどちらもこれを告知しますし、 +[Mudlet](/reference/clients/mudlet)にはそのための設定があります。 + +あるゲームが実際にそれに応じて動くかどうかは別の問題であり、当サイトに実測できる問題でもありません — +サーバーに、何を変えるつもりなのかを尋ねることはできないからです。 + +## ここでクローラーが負う務め + +クローラーはTTYPEで自分が何者かを名乗りますし、そうすべきです。当サイトのクローラーは情報用のURLを +添えて名乗るので、ログを読む管理者は、自分のゲームに接続してきたのが誰なのか、そしてやめてほしいと +伝えるにはどうすればよいのかを知ることができます。`ANSI`とだけ答えてそれ以上は何も答えないクローラーは、 +設計からして匿名であり、そうする正当な理由はありません。 + +## 当サイトが実測するもの + +当サイトとTTYPEをネゴシエートしたサーバーです。これは*当サイト*のほうが尋ねられる側になる数少ない +オプションの1つなので、ここの数値は、尋ねる気のあったサーバーの数だという点に注意してください。 diff --git a/content/reference/nl/client-atlantis.md b/content/reference/nl/client-atlantis.md new file mode 100644 index 0000000..45abd2b --- /dev/null +++ b/content/reference/nl/client-atlantis.md @@ -0,0 +1,47 @@ +--- +kind: client +slug: atlantis +title: Atlantis +summary: Een client die alleen op macOS draait, met een lang leven en een lange bèta. Van zijn scripting staat gedocumenteerd dat die niet meer werkt, en dat is de ene eerlijke "nee" in dit onderdeel. +home: https://www.riverdark.net/atlantis/ +platform: macOS +capability: screen reader | unknown | +capability: TLS | yes | https://www.riverdark.net/atlantis/history.php +capability: UTF-8 | yes | https://www.riverdark.net/atlantis/history.php +capability: MCCP | yes | https://www.riverdark.net/atlantis/history.php +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | no | https://www.riverdark.net/atlantis/ +see-also: clients/mudlet +see-also: protocols/charset +--- + +Atlantis is een native macOS-client die er al is sinds Mac OS X 10.3 en die in het Catalina-tijdperk +naar 64 bit is bijgewerkt. Hij kan overweg met tekensetonderhandeling volgens RFC 2066 en met +Unicode, wat beter is dan zijn leeftijd doet vermoeden, en hij doet MCCP en SSL. + +## De ene "nee" in dit onderdeel + +Zijn scripting liep via Perl, door de CamelBones-brug, en de eigen homepage van het project zegt dat +die niet meer werkt — Apple veranderde zijn omgang met Perl en de auteur van de bibliotheek is +enkele jaren geleden overleden. Dat is een *afwezigheid met bron*, en dat is iets anders dan een +onbekende; het is de enige cel in het hele clientonderdeel die er een draagt. Overal elders was het +eerlijke antwoord dat we het niet konden vaststellen. + +## Alles wat we niet konden vaststellen + +De versiegeschiedenis is volledig en openbaar en noemt **MCCP**, **SSL** en +**tekensetonderhandeling** — en noemt nergens GMCP, MSDP, ATCP of MSP. MXP komt één keer voor, als +iets dat bedoeld was voor een versie na 1.0.0, die er niet gekomen is. + +Er is een Perl-aanroep `Atlantis::Speak()` in de scripting-API, en het zou makkelijk zijn die te +lezen als ondersteuning voor schermlezers. Dat is het niet: het is een gescripte +tekst-naar-spraakaanroep in een scriptingsysteem waarvan het project zegt dat het niet werkt. +VoiceOver, "toegankelijk" en "schermlezer" komen niet voor op de homepage, de downloadpagina, de +volledige versiegeschiedenis of de gearchiveerde gebruikershandleiding. + +De huidige download is 0.9.9.8, nog altijd formeel een bèta, zonder dat er ergens op de site een +releasedatum gepubliceerd is. diff --git a/content/reference/nl/client-beipmu.md b/content/reference/nl/client-beipmu.md new file mode 100644 index 0000000..4df2b2c --- /dev/null +++ b/content/reference/nl/client-beipmu.md @@ -0,0 +1,47 @@ +--- +kind: client +slug: beipmu +title: BeipMU +summary: Een Windows-client gericht op de MUSH-kant van de hobby, met ondersteuning voor schermlezers in het uitvoervenster en Pueblo in plaats van MXP. +home: https://beipdev.github.io/BeipMU/ +platform: Windows +capability: screen reader | yes | https://github.com/BeipDev/BeipMU/blob/master/Assets/Changes.txt +capability: TLS | yes | https://beipdev.github.io/BeipMU/ +capability: UTF-8 | yes | https://beipdev.github.io/BeipMU/ +capability: MCCP | unknown | +capability: GMCP | yes | https://github.com/BeipDev/BeipMU/blob/master/Documentation/GMCP.md +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://beipdev.github.io/BeipMU/ +see-also: clients/mushclient +see-also: clients/potato +see-also: collaborative-roleplay +--- + +BeipMU is een Windows-client onder de MIT-licentie, die actief uitgebracht wordt, en een van de +weinige die gebouwd is met MUSH-achtig spel voor ogen in plaats van met gevecht-MUD's — meerdere +invoervensters, spawn windows, en een tekstengine die lange alinea's verwacht. Scripting gaat +standaard in JavaScript, met andere ActiveScript-engines beschikbaar. + +## Toegankelijkheid + +Het uitvoervenster implementeert de `IAccessible`-interface van Windows, bewust toegevoegd als stap +richting bruikbaarheid voor slechtziende spelers, en er is een **Speak**-triggeractie voor +tekst-naar-spraak. Nergens wordt een bepaalde schermlezer genoemd, en er is geen hoofdstuk over +toegankelijkheid in de documentatie. + +Eén waarschuwing als je gaat zoeken: een pagina in de eigen documentatie van het project zegt nog +steeds dat BeipMU geen spraaksynthese kan gebruiken. Die pagina is verouderd — de changelog en de +eigen issue-reacties van de onderhouder dateren beide van later. + +## Twee makkelijke misverstanden over deze client + +**BeipMU implementeert MCMP, niet MSP.** Het zijn verschillende protocollen met vergelijkbare namen +en vergelijkbare doelen, en de een als de ander lezen zou een bewering in deze tabel zetten die +niemand gedaan heeft. De MSP-rij zegt daarom onbekend. + +**Hij ondersteunt Pueblo, niet MXP.** Pueblo is het oudere schema voor HTML in een MUD en MXP het +latere; BeipMU documenteert basale Pueblo-stijlen en klikbare links. Over MXP is het niet +vastgesteld, in welke richting dan ook. diff --git a/content/reference/nl/client-blightmud.md b/content/reference/nl/client-blightmud.md new file mode 100644 index 0000000..9db52ff --- /dev/null +++ b/content/reference/nl/client-blightmud.md @@ -0,0 +1,50 @@ +--- +kind: client +slug: blightmud +title: Blightmud +summary: Een moderne terminalclient in Rust, met Lua-scripting, ingebouwde tekst-naar-spraak en een schermlezermodus die zichzelf aan de server bekendmaakt. +home: https://github.com/Blightmud/Blightmud +platform: Linux +platform: macOS +platform: Windows (WSL only) +capability: screen reader | yes | https://github.com/Blightmud/Blightmud +capability: TLS | yes | https://github.com/Blightmud/Blightmud +capability: UTF-8 | yes | https://github.com/Blightmud/Blightmud +capability: MCCP | yes | https://github.com/Blightmud/Blightmud +capability: GMCP | yes | https://github.com/Blightmud/Blightmud +capability: MSDP | yes | https://github.com/Blightmud/Blightmud +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/Blightmud/Blightmud +see-also: clients/tintin +see-also: clients/mudlet +see-also: protocols/ttype +--- + +Blightmud is een terminalclient geschreven in Rust, GPL 3, en behoort tot de actiefst uitgebrachte +clients in dit onderdeel. Scripting gaat in Lua. Hij draait alleen in de terminal: er is geen native +Windows-build, en Windows-gebruikers draaien hem onder WSL. + +## Toegankelijkheid + +Blightmud heeft hier drie afzonderlijke onderdelen, en dat is meer dan één rij kan dragen: + +- Een **schermlezervriendelijke modus** (`--reader-mode`, of de instelling `reader_mode`) die de + terminalinterface verandert in iets wat een lezer kan volgen. Het statusgebied wordt niet + ondersteund. +- **Ingebouwde tekst-naar-spraak**, als optionele compilatie, met een Lua-API die een script kan + gebruiken — inclusief een `tts.gag()` om te voorkomen dat een gevonden regel uitgesproken wordt. + De documentatie is er openhartig over dat zijn TTS naast een schermlezer draaien niet altijd een + gelukkige combinatie is. +- **Automatische MTTS-aankondiging**: in schermlezermodus of met TTS aan voegt hij + `MTTS_SCREEN_READER` toe aan wat hij de server over zichzelf vertelt, zodat een spel dat erom + geeft zich kan aanpassen. + +Net als bij TinTin++ wordt geen bepaalde schermlezer genoemd, dus dit is een gedocumenteerde modus +en geen geteste compatibiliteit met een product. + +## Waar de tabel onbekend zegt + +**MXP**, **MSP** en **ATCP** komen nergens voor in de README van het project of in de meegeleverde +help. **MCCP** is gedocumenteerd als v2; of v1 ook aangekund wordt hebben we niet vastgesteld. diff --git a/content/reference/nl/client-mudlet.md b/content/reference/nl/client-mudlet.md new file mode 100644 index 0000000..205d7dc --- /dev/null +++ b/content/reference/nl/client-mudlet.md @@ -0,0 +1,57 @@ +--- +kind: client +slug: mudlet +title: Mudlet +summary: Platformonafhankelijk, met Lua-scripting, en de client met de grondigst gedocumenteerde ondersteuning voor schermlezers in dit onderdeel. +home: https://www.mudlet.org/ +platform: Windows +platform: macOS +platform: Linux +capability: screen reader | yes | https://wiki.mudlet.org/w/Manual:Screen_Readers +capability: TLS | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: UTF-8 | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MCCP | unknown | +capability: GMCP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MSDP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: ATCP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MXP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MSP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: scripting | yes | https://github.com/Mudlet/Mudlet +see-also: clients/blightmud +see-also: clients/tintin +see-also: protocols/gmcp +see-also: connecting +--- + +Mudlet is een grafische client met een kaartfunctie, een pakketsysteem en een Lua-API waartegen het +grootste deel van zijn eigen functies geschreven is. Hij is GPL, wordt actief uitgebracht, en is de +gebruikelijke aanbeveling voor wie op een moderne gevecht-MUD begint. + +## Toegankelijkheid + +Dit is de client met de sterkste gedocumenteerde papieren in dit onderdeel, en het is de moeite +waard uit te spellen wat "gedocumenteerd" hier betekent, want het is ongewoon. + +Mudlet heeft een **hoofdstuk over schermlezers in de handleiding**, pagina's per besturingssysteem +die Narrator, NVDA en JAWS op Windows, Orca op Linux en VoiceOver op macOS bij naam noemen, een +commando `mudlet access on` in de client zelf, en een optie om binnenkomende speltekst via de lezer +aan te kondigen. Er is ook een instelling die het gebruik van een schermlezer via MTTS aan de server +aankondigt, zodat een spel zich kan aanpassen als het dat wil. + +Het is ook openhartig over waar het niet goed werkt: de eigen Windows-pagina zegt dat JAWS het +uitvoervenster niet leest zoals andere lezers dat doen, en beveelt in plaats daarvan Narrator of +NVDA aan. Een project dat het geval publiceert waarin zijn toegankelijkheidsondersteuning onwerkbaar +is, geeft je betere informatie dan een project dat een vinkje publiceert. + +## Waar de tabel onbekend zegt + +**MCCP.** De broncode van Mudlet implementeert MCCP v1 en v2, maar de pagina met ondersteunde +protocollen in de handleiding noemt het niet, en de regel in dit onderdeel is dat een uitspraak over +een mogelijkheid de eigen documentatie van het project aanhaalt. Een constante uit een headerbestand +lezen is niet dezelfde handeling, dus de cel zegt onbekend. + +## Aantekening over codering + +De standaardcodering van Mudlet voor servergegevens is ASCII in plaats van UTF-8, en +CHARSET-onderhandeling kwam in 4.10. Komt de tekst van een spel er verkeerd uit op een vers profiel, +dan is die instelling de eerste plek om te kijken. diff --git a/content/reference/nl/client-mushclient.md b/content/reference/nl/client-mushclient.md new file mode 100644 index 0000000..4118a0f --- /dev/null +++ b/content/reference/nl/client-mushclient.md @@ -0,0 +1,53 @@ +--- +kind: client +slug: mushclient +title: MUSHclient +summary: De al lang gevestigde Windows-client. Vijf scripttalen, een plug-inarchitectuur waar het grootste deel van zijn protocolondersteuning in zit, en een releasegeschiedenis die vertraagd is. +home: https://www.mushclient.com/ +platform: Windows +platform: Linux (Wine) +capability: screen reader | unknown | +capability: TLS | unknown | +capability: UTF-8 | unknown | +capability: MCCP | yes | https://www.mushclient.com/mushclient/mccp.htm +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | yes | https://www.mushclient.com/gmcp +capability: MXP | yes | https://www.mushclient.com/mushclient/doc/general/features.html +capability: MSP | yes | https://github.com/nickgammon/mushclient/blob/master/plugins/msp.xml +capability: scripting | yes | https://www.mushclient.com/mushclient/doc/general/features.html +see-also: clients/mudlet +see-also: clients/potato +see-also: protocols/mccp +--- + +MUSHclient is de Windows-client van Nick Gammon, onder de MIT-licentie, en lange tijd het +standaardantwoord voor iedereen op Windows. Er wordt in gescript met Lua, VBScript, JScript, +PerlScript en Python, en veel van wat hij doet wordt gedragen door plug-ins in plaats van door de +kern — wat een echte architectuurkeuze is en tegelijk de reden dat verscheidene rijen hierboven +lastiger te beantwoorden zijn dan ze lijken. + +De laatste getagde release is **5.06, uit maart 2019**. Er wordt nog steeds aan de repository +gecommit, en er zijn release notes voor een 5.07 die niet uitgebracht is. + +## Waarom zoveel rijen onbekend zeggen + +Bij elk daarvan is het eerlijke antwoord "we konden het niet vaststellen", en de redenen +verschillen: + +- **GMCP** — de eigen pagina van het project erover toont een *voorbeeld* van een plug-in die je zou + kunnen schrijven, niet een functie die de client heeft. Dat is iets anders dan ondersteuning + uitleveren, dus de cel zegt onbekend in plaats van ja. +- **TLS** — de gedocumenteerde methode is een extern `stunnel`-proces. Een commit die TLS op basis + van OpenSSL toevoegde belandde in 2026 op de master-branch en zit in geen enkele release, dus er + is niets dat een gebruiker vandaag kan installeren en waar wij naar kunnen wijzen. +- **UTF-8** — CHARSET-onderhandeling komt voor in de niet-uitgebrachte 5.07-notities en nergens waar + wij het in de documentatie van een uitgebrachte versie konden vinden. +- **MSDP** — niets in welke richting dan ook. +- **Schermlezer** — er wordt een tekst-naar-spraak-plug-in op basis van Windows SAPI met de client + meegeleverd, en dat is niet hetzelfde als ondersteuning voor schermlezers. Er is geen paragraaf + over toegankelijkheid in de handleiding, en de auteur heeft in zijn eigen forum beschreven waarom + het uitvoervenster lastig werkbaar is voor een lezer: het kent geen begrip van een huidige regel. + We konden geen antwoord vaststellen, dus de tabel geeft er geen. + +Geen van deze is een *nee*. Verscheidene zijn heel goed mogelijk ja en we konden het niet aantonen. diff --git a/content/reference/nl/client-potato.md b/content/reference/nl/client-potato.md new file mode 100644 index 0000000..48d1df0 --- /dev/null +++ b/content/reference/nl/client-potato.md @@ -0,0 +1,48 @@ +--- +kind: client +slug: potato +title: Potato MUSHclient +summary: Een platformonafhankelijke Tcl/Tk-client geschreven voor MUSH-spelers. Goede ondersteuning voor codering, en documentatie die over de meeste protocollen helemaal niets zegt. +home: https://www.potatomushclient.com/ +platform: Windows +platform: Linux +platform: macOS +capability: screen reader | unknown | +capability: TLS | yes | https://github.com/potatomushclient/potato/wiki/ConfigureWorldsBasics +capability: UTF-8 | yes | https://github.com/potatomushclient/potato/wiki/Features +capability: MCCP | unknown | +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/potatomushclient/potato/wiki/FAQs +see-also: clients/beipmu +see-also: clients/mushclient +see-also: collaborative-roleplay +--- + +Potato is een Tcl/Tk-client gebouwd voor MUSH-spel — meerdere werelden, spawn windows, en een set +standaardinstellingen die ervan uitgaat dat je poses typt en geen gevechtscommando's. Hij draait +vanuit dezelfde broncode op Windows, Linux en macOS, waarbij de macOS-builds meestal een versie of +twee achterlopen. + +Hij onderhandelt over tekencodering en spreekt volledig Unicode, wat voor de MUSH-kant van de hobby +de mogelijkheid is die er in de praktijk het meest toe doet. + +Let op één gedocumenteerde beperking: hij ondersteunt verbinden met een poort die vanaf het begin +SSL is, en zijn eigen configuratiepagina zegt dat onderhandelde SSL in STARTTLS-stijl **niet** +ondersteund wordt. + +## Waarom zes rijen onbekend zeggen + +We hebben de homepage van het project, zijn downloadpagina, alle 103 helpbestanden van zijn wiki en +zijn hele bronboom doorzocht op GMCP, MSDP, MCCP, MXP, MSP en ATCP. Er is over geen ervan een +gedocumenteerde uitspraak. Er is wél *code* die enkele ervan aanraakt, en dit onderdeel maakt van +code geen uitspraak over een mogelijkheid — een tabel die "ja" zegt op grond van een constante in +een headerbestand doet een belofte die het project nooit gedaan heeft. + +De rij over schermlezers is hetzelfde antwoord, langs dezelfde weg bereikt: een zoektocht zonder +onderscheid tussen hoofd- en kleine letters naar "screen reader", "text-to-speech", NVDA, JAWS, +VoiceOver, "accessibility", "visually impaired" en "blind" door alles wat het project publiceert, +leverde helemaal niets op. Dat is geen bevinding over de software. diff --git a/content/reference/nl/client-tintin.md b/content/reference/nl/client-tintin.md new file mode 100644 index 0000000..99365bf --- /dev/null +++ b/content/reference/nl/client-tintin.md @@ -0,0 +1,53 @@ +--- +kind: client +slug: tintin +title: TinTin++ +summary: Een terminalclient met een eigen scripttaal, op elk platform inclusief telefoons, en een gedocumenteerde schermlezermodus. +home: https://tintin.mudhalla.net/ +platform: Linux +platform: macOS +platform: Windows +platform: Android +platform: iOS +capability: screen reader | yes | https://tintin.mudhalla.net/manual/screen_reader.php +capability: TLS | yes | https://github.com/scandum/tintin +capability: UTF-8 | yes | https://github.com/scandum/tintin +capability: MCCP | yes | https://tintin.mudhalla.net/ +capability: GMCP | yes | https://tintin.mudhalla.net/manual/event.php +capability: MSDP | yes | https://tintin.mudhalla.net/manual/msdp.php +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/scandum/tintin +see-also: clients/blightmud +see-also: clients/mudlet +see-also: protocols/msdp +see-also: protocols/ttype +--- + +TinTin++ is een client voor de opdrachtregel, GPL 3, wordt actief uitgebracht, en draait op meer +plekken dan wat dan ook hier — waaronder Android en iOS. De scripttaal is een eigen taal, beknopt, +en tot heel veel in staat; een aanzienlijk deel van wat andere clients in de grafische interface +doen is hier een `#config`-regel. + +Dezelfde auteur onderhoudt de protocolspecificaties voor **MSSP** en **MSDP**, en daarom halen +zoveel van de protocolpagina's in dit onderdeel dezelfde site aan. + +## Toegankelijkheid + +TinTin++ heeft een eigen handleidingpagina voor de **schermlezermodus** (`#config screen reader on`, +of `-s` bij het starten). Die aanzetten doet twee dingen: het verwijdert of verandert visuele +elementen die hardop voorgelezen nergens op slaan, en het meldt het gebruik van een schermlezer aan +de server via [MTTS](/reference/protocols/ttype), zodat een spel zijn eigen uitvoer kan aanpassen. + +Dat is een gedocumenteerde modus, geen bewering dat er met een bepaalde lezer getest is — op de +pagina wordt geen product genoemd. Het is merkbaar zwakker bewijs dan een client die de lezers noemt +waarmee hij werkt, en merkbaar sterker dan niets. + +## Waar de tabel onbekend zegt + +Voor **MXP** en **MSP** bestaan er allebei scripts uit de gemeenschap op de site van het project, en +een script is niet de client die een protocol ondersteunt — dat van MXP zegt ronduit dat het +misschien niet op elke MUD werkt. Ingebouwde ondersteuning voor een van beide is niet vastgesteld. +Over **ATCP** vonden we in geen enkele richting iets; merk op dat ATCP grotendeels vervangen is door +GMCP, en dat ondersteunt TinTin++ wel. diff --git a/content/reference/nl/client-tinyfugue.md b/content/reference/nl/client-tinyfugue.md new file mode 100644 index 0000000..19bb835 --- /dev/null +++ b/content/reference/nl/client-tinyfugue.md @@ -0,0 +1,46 @@ +--- +kind: client +slug: tinyfugue +title: TinyFugue +summary: De klassieke UNIX-terminalclient. Upstream heeft sinds 2007 niets uitgebracht; een onderhouden fork draagt hem verder. +home: https://tinyfugue.sourceforge.net/ +platform: Linux +platform: macOS +platform: BSD +capability: screen reader | unknown | +capability: TLS | yes | https://tinyfugue.sourceforge.net/ +capability: UTF-8 | unknown | +capability: MCCP | yes | https://tinyfugue.sourceforge.net/ +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://tinyfugue.sourceforge.net/ +see-also: clients/tintin +see-also: clients/blightmud +--- + +TinyFugue — "tf" — is de terminalclient die een groot deel van de MUSH-wereld twee decennia lang +gebruikt heeft, met aparte deelvensters voor invoer en uitvoer, een eigen macrotaal, en een stel +gewoonten die verscheidene van zijn concurrenten overleefd hebben. + +**Upstream ligt stil**: de laatste release is 5.0 bèta 8, van januari 2007. Hij bouwt nog steeds en +hij werkt nog steeds. + +Een onderhouden fork, *TinyFugue Rebirth*, wordt actief uitgebracht en voegt GMCP, ATCP, +ondersteuning voor brede tekens via ICU, en scripting in Python en Lua naast de eigen macrotaal toe. +De tabel hierboven beschrijft **upstream**, want dat is waar "TinyFugue" naar verwijst; installeer +je vandaag, dan is de fork het eerst bekijken waard. + +## De valstrik in de documentatie van deze client + +Upstream heeft een documentatieonderwerp met de naam **"non-visual mode"**. Dat gaat niet over +hulptechnologie — het gaat erover de invoer op de onderste regel te houden — en het noemt nergens +een schermlezer, spraak of blinde gebruikers. Een mogelijkhedentabel die met trefwoorden bij elkaar +gezocht is, zou van die bestandsnaam een ja maken. Deze zegt onbekend, want dat is wat de +documentatie draagt. + +UTF-8 is een antwoord van dezelfde vorm: de gedocumenteerde ondersteuning voor codering geldt de +8-bits ISO 8859-tekensets, en we vonden bij upstream in geen enkele richting een uitspraak over +UTF-8. diff --git a/content/reference/nl/client-vipmud.md b/content/reference/nl/client-vipmud.md new file mode 100644 index 0000000..c7442c3 --- /dev/null +++ b/content/reference/nl/client-vipmud.md @@ -0,0 +1,43 @@ +--- +kind: client +slug: vipmud +title: VIP Mud +summary: Een commerciële Windows-client die van de grond af voor blinde spelers gebouwd is. Hij noemt zeven schermlezers — en publiceert vrijwel niets over zijn protocolondersteuning. +home: https://www.gmagames.com/vipmud.shtml +platform: Windows +capability: screen reader | yes | https://www.gmagames.com/vipmud.shtml +capability: TLS | unknown | +capability: UTF-8 | unknown | +capability: MCCP | unknown | +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | yes | https://www.gmagames.com/vipmud.shtml +capability: scripting | yes | https://www.gmagames.com/vipmud.shtml +see-also: clients/mudlet +see-also: clients/blightmud +--- + +VIP Mud is de ene client in dit onderdeel waarvan het *hele* ontwerpuitgangspunt toegankelijkheid +is. Hij is commercieel — dertig dollar, met een volledige proefperiode van dertig dagen waarna hij +blijft werken met een beperkte set functies — en het is een Windows-programma. + +Het is met afstand de sterkste aanspraak op toegankelijkheid hier, en ongewoon genoeg is die +specifiek. De productpagina noemt **JAWS, Window-Eyes, System Access, NVDA, Cobra, SuperNova/Hal en +Microsoft SAPI** als werkend zonder verdere ingrepen, en beschrijft functies die alleen ergens op +slaan als je goed over het probleem hebt nagedacht: verschillende stemmen per venster en per soort +uitvoer, spam uit de spraak weren terwijl die nog wel getoond wordt, en verscheidene manieren om +ASCII-kunst te onderdrukken — het meest vijandige dat een MUD naar een schermlezer stuurt. + +## Waarom de rest van de tabel leeg is + +Omdat de leverancier een marketingpagina publiceert en geen handleiding. Niets daarop noemt GMCP, +MSDP, MCCP, MXP, ATCP, TLS of tekencodering; het beschrijft het product als "a Telnet-based client" +en laat het daarbij. **Negen keer onbekend op een rij is geen oordeel over de software.** Het is hoe +een matrix eruitziet wanneer de enige beschikbare bron één pagina is, en het als negen keer nee +publiceren zou een leugen zijn over een product dat het allemaal heel goed zou kunnen doen. + +Nog twee dingen die we niet konden vaststellen: enige releasedatum voor de huidige versie, en of hij +nog actief ontwikkeld wordt — de leverancier is in februari 2025 overgenomen, en de productpagina +draagt een copyright uit 2016. diff --git a/content/reference/nl/codebase-aresmush.md b/content/reference/nl/codebase-aresmush.md new file mode 100644 index 0000000..eb69b88 --- /dev/null +++ b/content/reference/nl/codebase-aresmush.md @@ -0,0 +1,34 @@ +--- +kind: codebase +slug: aresmush +title: AresMUSH +summary: Een moderne rollenspelserver in Ruby, met een webfront-end en scènegereedschap ingebouwd in plaats van in softcode geschreven. +codebase: AresMUSH +home: https://aresmush.com/ +see-also: collaborative-roleplay +see-also: codebases/pennmush +see-also: codebases/evennia +--- + +AresMUSH is de nieuwste veelgebruikte server die zich volledig op **gezamenlijk rollenspel** richt, +en neemt een andere positie in dan de TinyMUSH-lijn die hij opvolgt. Waar een PennMUSH-spel zijn +scènesysteem, zijn personagebladen en zijn takenwachtrij opbouwt uit softcode die geschreven is door +wie er toevallig was, levert Ares die als kant-en-klare functies mee en verwacht het dat de staf van +een spel ze instelt in plaats van programmeert. + +Het komt met een **webportaal** — personagewiki's, scènelogs, forums en het spel zelf, allemaal +bereikbaar vanuit een browser — wat voor een genre waarin mensen de logs achteraf lezen een verschil +in soort is en niet in graad. + +De configuratie staat in YAML; uitbreidingen zijn Ruby-plug-ins. Er is geen programmeertaal in het +spel voor spelers, en dat is de ruil: minder touw, minder touwgerelateerd letsel, en minder van de +improviserende bouwcultuur waar de MUSH-lijn haar naam aan dankt. + +## Hoe het er van buitenaf uitziet + +Geen MSSP. Het beantwoordt een `WHO` vóór het inloggen, en dat antwoord is een **lijst per speler** +in plaats van een kaal getal, dat onze parser op structuur telt. Op het spel dat we gemeten hebben +werden geen telnet-opties onderhandeld. + +Kies je tussen dit en PennMUSH voor een nieuw rollenspel, dan is de vraag ruwweg of je een systeem +wilt dat je instelt of een systeem dat je schrijft. diff --git a/content/reference/nl/codebase-circlemud.md b/content/reference/nl/codebase-circlemud.md new file mode 100644 index 0000000..41ef132 --- /dev/null +++ b/content/reference/nl/codebase-circlemud.md @@ -0,0 +1,28 @@ +--- +kind: codebase +slug: circlemud +title: CircleMUD +summary: De DikuMUD-afgeleide die goed genoeg gedocumenteerd was om les mee te geven. Enorm veel geforkt; draait nog steeds. +codebase: CircleMUD +home: https://www.circlemud.org/ +see-also: codebases/dikumud +see-also: codebases/tbamud +see-also: codebases/rom +--- + +CircleMUD is een DikuMUD-afgeleide waarvan het onderscheidende kenmerk nooit een spelmechaniek was: +het was de **documentatie**. De uitgave van Jeremy Elson was schoon, van commentaar voorzien en +vergezeld van een programmeergids, en daardoor werd het de codebase waarop mensen C leerden, een MUD +draaiden en forkten zonder eerst iets te hoeven reverse-engineeren. + +Het gevolg is dat een zeer groot aantal draaiende spellen Circle-afgeleiden zijn die er verscheidene +generaties van verwijderd liggen, vaak zonder dat het woord ergens voorkomt waar een speler het +ziet. + +De ontwikkeling van Circle zelf is allang afgerond; **tbaMUD** is de voortzetting, en een +Circle-spel dat vandaag onderhouden wordt, wordt meestal als tbaMUD onderhouden. + +## Hoe het er van buitenaf uitziet + +MSSP, beantwoord op verzoek. Geen `WHO` op het inlogscherm — de Diku-familie biedt die over het +algemeen niet aan, en dat is de reden dat een gids die alleen inlogschermen leest hier niets ziet. diff --git a/content/reference/nl/codebase-cobramush.md b/content/reference/nl/codebase-cobramush.md new file mode 100644 index 0000000..d0dd681 --- /dev/null +++ b/content/reference/nl/codebase-cobramush.md @@ -0,0 +1,25 @@ +--- +kind: codebase +slug: cobramush +title: CobraMUSH +summary: Een PennMUSH-fork met een eigen model van divisies en bevoegdheden. Kleine verspreiding, antwoordt nog steeds. +codebase: CobraMUSH +home: https://cobramush.org/ +see-also: codebases/pennmush +see-also: codebases/rhostmush +--- + +CobraMUSH is van PennMUSH afgesplitst en voegde een *divisie*model toe — een hiërarchie van +administratief gezag met delegeerbare bevoegdheden, in plaats van het vlakke onderscheid tussen +wizard en royalty dat zijn ouder hanteert. Spellen die stukjes stafbevoegdheid willen uitdelen +zonder alles uit te delen, zijn zijn publiek. + +Softcode die voor PennMUSH geschreven is draait grotendeels, en de verschillen zitten geconcentreerd +in precies het gebied waar de fork om begonnen was. + +## Hoe het er van buitenaf uitziet + +Geen MSSP, een werkende `WHO` vóór het inloggen, en op het spel dat we gemeten hebben werden er +helemaal geen telnet-opties onderhandeld. Dat laatste is geen kritiek: een server die nergens over +onderhandelt is een server die onderhandeling niet fout kan doen, en platte tekst over een platte +socket is het ene ding dat elke client in deze hobby aankan. diff --git a/content/reference/nl/codebase-coffeemud.md b/content/reference/nl/codebase-coffeemud.md new file mode 100644 index 0000000..2dfe2be --- /dev/null +++ b/content/reference/nl/codebase-coffeemud.md @@ -0,0 +1,29 @@ +--- +kind: codebase +slug: coffeemud +title: CoffeeMUD +summary: Een MUD-server in Java, met het grootste MSSP-rapport van alles wat we gepeild hebben en een ongewoon breed protocoloppervlak. +codebase: CoffeeMUD +home: https://www.coffeemud.net/ +see-also: codebases/dikumud +see-also: protocols/mssp +--- + +CoffeeMUD is een MUD-server in Java met een ongewoon breed functieoppervlak — hij komt met een eigen +webserver, mail, forums en een groot klassen- en vaardighedensysteem, en het is een van de weinige +servers in de hobby die niet in C geschreven is. + +Hij wordt actief onderhouden, wat naar de maatstaven van dit deel van de catalogus het hardop zeggen +waard is. + +## Hoe het er van buitenaf uitziet + +MSSP en **MCCP2**, en CoffeeMUD is een van de slechts drie servers van de twintig die we probeerden +die ook de *platte-tekstvorm* `MSSP-REQUEST` beantwoordde — een variant die ouder is dan de +telnet-optie en die je nog af en toe tegenkomt. + +Zijn MSSP-rapport is het grootste dat we gemeten hebben: **47 velden**, waaronder `PORT` dat negen +keer afzonderlijk gemeld wordt voor negen afzonderlijke poorten. Dat is geen misvorming. +MSSP-variabelen zijn lijsten, en een crawler die een meerwaardige `PORT` platslaat tot één string +produceert het getal `80234201` uit `"80" "23" "4201"` — een bug die dit project heeft uitgeleverd +en hersteld, en de reden dat de parser hier waarden overal als lijsten bewaart. diff --git a/content/reference/nl/codebase-dikumud.md b/content/reference/nl/codebase-dikumud.md new file mode 100644 index 0000000..99251a2 --- /dev/null +++ b/content/reference/nl/codebase-dikumud.md @@ -0,0 +1,36 @@ +--- +kind: codebase +slug: dikumud +title: DikuMUD +summary: De wortel van de familie gevecht-MUD's. Levels, klassen, uitrusting en areabestanden — en een licentie die een generatie afgeleiden vormgaf. +codebase: DikuMUD +home: https://dikumud.com/ +see-also: codebases/circlemud +see-also: codebases/rom +see-also: codebases/smaug +see-also: mush-mud-muck-moo +--- + +DikuMUD, geschreven aan het Datalogisk Institut van de Universiteit van Kopenhagen en uitgebracht in +1991, is de voorouder van het meeste waar mensen op doelen als ze zonder nadere aanduiding "MUD" +zeggen. Levels, personageklassen, hit points, mobs, uitrustingsplekken, een areabestandsformaat dat +een bouwer offline schrijft — de hele woordenschat komt hiervandaan, en spellen die nooit +Diku-broncode gezien hebben erven nog altijd zijn vorm. + +De licentie is deel van het verhaal. Diku was vrij te gebruiken maar verbood geld vragen voor +toegang en eiste dat de oorspronkelijke credits getoond werden, en door die clausule verschijnen "de +Diku-credits" op het inlogscherm van spellen die er verscheidene forks van verwijderd liggen. + +De directe afstammelingen — **Merc**, daarna **ROM**, **CircleMUD**, **SMAUG**, **tbaMUD** en +tientallen andere — vormen een groot deel van elke MUD-lijst die ooit bestaan heeft. + +## Hoe het er van buitenaf uitziet + +De Diku-familie is de **MSSP**-familie. Waar de MUSH-kant een telling publiceert via een `WHO` op +het inlogscherm en helemaal geen MSSP aanbiedt, beantwoorden servers uit de Diku-lijn overweldigend +vaak telnet-optie 70 met een gestructureerd rapport, en daar komen hun getallen hier vandaan. + +**MCCP2** — streamcompressie — komt in deze familie ook veel voor, en het is de moeite waard te +weten dat een client die erover onderhandelt maar de stream niet kan uitpakken het hele +verbindingsscherm als binaire ruis ontvangt. Dat was een echt gebrek in de eigen telnet-bibliotheek +van dit project en het is hersteld; zie [MCCP](/reference/protocols/mccp). diff --git a/content/reference/nl/codebase-evennia.md b/content/reference/nl/codebase-evennia.md new file mode 100644 index 0000000..722c38d --- /dev/null +++ b/content/reference/nl/codebase-evennia.md @@ -0,0 +1,34 @@ +--- +kind: codebase +slug: evennia +title: Evennia +summary: Een Python-framework in plaats van een afgerond spel. Twee Evennia-spellen kunnen niets gemeen hebben behalve het leidingwerk. +codebase: Evennia +home: https://www.evennia.com/ +see-also: codebases/aresmush +see-also: collaborative-roleplay +see-also: protocols/gmcp +--- + +Evennia is een **MU\*-framework**, geen spel — dat is het eerste wat je erover moet weten en het is +wat Evennia-spellen onderling vergelijken zinloos maakt. Het is een Python-bibliotheek gebouwd op +Django en Twisted die je accounts, objecten, kamers, commando's, een persistentielaag en de +netwerkstack geeft, en die vervolgens verwacht dat jij het spel schrijft. + +Het gevolg is dat "draait op Evennia" je veel minder over een spel vertelt dan "draait op PennMUSH" +doet. Er zijn gevecht-MUD's op Evennia en er zijn rollenspellen op Evennia en ze delen geen enkele +woordenschat. Twee Evennia-spellen hebben mogelijk geen enkel commando gemeen. + +Voor een ontwikkelaar die al Python kent is dit de kortste weg van niets naar een draaiende wereld, +en het is waar een flink deel van de nieuwe spellen sinds het midden van de jaren 2010 begonnen is. + +## Hoe het er van buitenaf uitziet + +Evennia biedt **MSSP** aan, en publiceert daarmee een spelerstelling. Op het spel dat we gemeten +hebben onderhandelde het ook over **MCCP2** — compressie — wat kenmerkend is voor een stack die zijn +telnet serieus nam. + +Omdat Evennia een framework is, is wat een bepaald spel onderhandelt deels de beslissing van dat +spel. De adoptiecijfers op de protocolpagina's zijn tellingen van wat servers ons daadwerkelijk +aangeboden hebben, niet van wat het framework kan, en voor Evennia liggen die twee verder uit elkaar +dan voor de meeste. diff --git a/content/reference/nl/codebase-fluffos.md b/content/reference/nl/codebase-fluffos.md new file mode 100644 index 0000000..dc626d4 --- /dev/null +++ b/content/reference/nl/codebase-fluffos.md @@ -0,0 +1,34 @@ +--- +kind: codebase +slug: fluffos +title: FluffOS +summary: De onderhouden opvolger van MudOS, en de driver waarop de meeste overgebleven LPMud-spellen draaien. Het spel is in LPC geschreven, niet in C. +codebase: FluffOS +home: https://www.fluffos.info/ +see-also: codebases/dikumud +see-also: mush-mud-muck-moo +--- + +De LPMud-traditie deelt de wereld anders in dan Diku. Er is een **driver** — een C-programma dat een +objectgeoriënteerde interpreter draait — en een **mudlib**, en die is het hele spel, geschreven in +**LPC** en geladen door de driver. Kamers, gevecht, commando's en de inlogreeks zijn allemaal +mudlib-objecten; de driver weet van geen van alle. + +Dat maakt een LPMud in geest dichter bij een MUSH dan zijn gevechtssystemen doen vermoeden: het spel +is geschreven in een taal die binnen het spel leeft, en twee LPMuds die een driver delen delen +mogelijk niets anders. + +**MudOS** was jarenlang de dominante driver; **FluffOS** is de onderhouden voortzetting ervan en is +waar een draaiend LP-spel vandaag het meest waarschijnlijk op draait. Bekende mudlibs — Nightmare, +Lima, die van Discworld zelf — zijn opnieuw aparte projecten. + +## Hoe het er van buitenaf uitziet + +MSSP en **MCCP2** op het FluffOS-spel dat we gemeten hebben. MudOS was een van slechts twee +codebases in ons onderzoek die *zowel* MSSP als een `WHO` op het inlogscherm beantwoordde, al was de +`WHO` die het gaf een opsomming per speler in plaats van een telling. + +Omdat de mudlib het spel is, is wat een bepaald LP-spel onderhandelt evenzeer een beslissing van de +mudlib als van de driver — de adoptiecijfers op de protocolpagina's tellen wat servers ons +daadwerkelijk aangeboden hebben, wat voor deze familie een zwakker signaal over de codebase is dan +elders. diff --git a/content/reference/nl/codebase-moo.md b/content/reference/nl/codebase-moo.md new file mode 100644 index 0000000..af76b53 --- /dev/null +++ b/content/reference/nl/codebase-moo.md @@ -0,0 +1,29 @@ +--- +kind: codebase +slug: moo +title: MOO +summary: Objectgeoriënteerd, volledig van binnenuit bewerkt, en evenzeer een onderzoeks- en onderwijsplatform als een spelengine. +codebase: MOO +home: https://www.ipomoea.org/moo/ +see-also: mush-mud-muck-moo +see-also: codebases/muck +--- + +MOO — *MUD, Object-Oriented* — voert het idee dat "de wereld zichzelf bewerkt" verder door dan wat +dan ook in de hobby. LambdaMOO, de oorspronkelijke server, levert een kleine C-kern en een database; +vrijwel alles wat een gebruiker ervaart is geschreven **in de MOO-taal, binnen de draaiende +database, door de mensen die haar gebruiken**. Er is geen bronbestand voor een kamer. + +Die eigenschap gaf MOO's een leven buiten spellen. Door de jaren negentig heen werden ze gebruikt +voor onderwijs, conferenties en onderzoek — Diversity University, BioMOO, Jay's House — en de +technische literatuur over MOO is onevenredig academisch voor een codebase in deze hoek. + +De verspreiding is vandaag klein maar werkelijk niet nul, en de servers die er nog zijn draaien vaak +al tientallen jaren onafgebroken. + +## Hoe het er van buitenaf uitziet + +Geen MSSP, en geen `WHO` die we konden ontleden op het spel dat we gemeten hebben. Wat het wel had +was een zin in zijn verbindingsscherm die luidde *"one of three players are active"* — en daar komt +de lezer voor voluit geschreven getallen in deze crawler vandaan. Een parser die alleen naar cijfers +kijkt ziet daar helemaal geen telling, en zou dat spel voor altijd als onbekend gemeld hebben. diff --git a/content/reference/nl/codebase-muck.md b/content/reference/nl/codebase-muck.md new file mode 100644 index 0000000..0e1e6b9 --- /dev/null +++ b/content/reference/nl/codebase-muck.md @@ -0,0 +1,31 @@ +--- +kind: codebase +slug: muck +title: MUCK +summary: Een TinyMUD-afstammeling met een eigen Forth-achtige taal in het spel, en een sociale cultuur die van de MUSH-kant verschilt. +codebase: MUCK +home: https://www.fuzzball.org/ +see-also: mush-mud-muck-moo +see-also: codebases/tinymush +see-also: codebases/moo +--- + +MUCK — in de praktijk vrijwel altijd **Fuzzball MUCK** — is eerder een broer of zus van de MUSH-lijn +dan een afstammeling ervan: beide komen van TinyMUD, en beide zetten een programmeertaal binnen in +het spel. + +De taal is het zichtbare verschil. MUF (*Multi-User Forth*) is stapelgebaseerd en leest in niets als +MUSH-softcode; een bouwer die de ene vloeiend beheerst is beginner in de andere. Daarboven zit MPI, +een kleinere taal voor inline-expressies die gebruikt wordt voor de dingen die softcode op een MUSH +zou doen. + +Cultureel is MUCK het thuis van een groot deel van de sociale en fandomwerelden van de hobby. Die +spellen zijn meestal gebouwd rond aanwezigheid en gesprek in plaats van rond scènes met een begin en +een eind, wat een echt verschil met de rollenspel-MUSH-traditie is en geen kwestie van thema. + +## Hoe het er van buitenaf uitziet + +Geen MSSP. Een `WHO` vóór het inloggen die met een telling antwoordt. Geen telnet-opties +onderhandeld op het spel dat we gemeten hebben — en één detail uit het onderzoek is het bewaren +waard: zijn `WHO`-antwoord eindigde op een spatie zonder regeleinde, en dat is het soort ding +waardoor een naïeve parser helemaal niets meldt. diff --git a/content/reference/nl/codebase-pennmush.md b/content/reference/nl/codebase-pennmush.md new file mode 100644 index 0000000..b1625b6 --- /dev/null +++ b/content/reference/nl/codebase-pennmush.md @@ -0,0 +1,52 @@ +--- +kind: codebase +slug: pennmush +title: PennMUSH +summary: De meest verspreide MUSH-server. Softcode, een lange releasegeschiedenis, en een van slechts twee codebases in ons onderzoek die zowel MSSP als een WHO vóór het inloggen beantwoorden. +codebase: PennMUSH +home: https://www.pennmush.org/ +see-also: codebases/tinymux +see-also: codebases/rhostmush +see-also: codebases/cobramush +see-also: mush-mud-muck-moo +see-also: protocols/mssp +--- + +PennMUSH stamt via een fork uit 1991 af van TinyMUSH, en het is de server waarop de meeste +langlopende rollenspel-MUSHes draaien. Zijn bepalende kenmerk is **softcode**: een functionele +expressietaal, van binnen het spel bewerkt door iedereen met de juiste bit gezet, waarin een groot +deel van het gedrag van een willekeurige MUSH geschreven is. Een PennMUSH-spel wordt niet zozeer +ingesteld als wel geprogrammeerd door zijn spelers. + +Versies lezen als `1.8.8p0` — een major, een minor en een patchlevel — en de patchlevel beweegt +vaak. Spellen draaien geregeld een versie die enkele patchlevels achterloopt, wat niets bijzonders +is. + +## Hoe het er van buitenaf uitziet + +PennMUSH is een van slechts twee codebases in ons eigen onderzoek onder 38 servers die *beide* +routes beantwoordde die wij peilen. Het biedt MSSP aan wanneer erom gevraagd wordt, en het +beantwoordt een `WHO` die op het inlogscherm getypt wordt, en op het spel dat we gemeten hebben +waren die twee het eens — wat zeldzamer is dan het klinkt, en waardoor PennMUSH de controle werd +waartegen we andere servers getoetst hebben. + +De `WHO` vóór het inloggen doet meer dan gemak: het is de manier waarop de MUSH-familie überhaupt +een spelerstelling publiceert, aangezien de rest van de familie meestal geen enkele MSSP aanbiedt. +Zie [MSSP](/reference/protocols/mssp) voor waarom die scheiding de reden is dat deze site vier lagen +peilt in plaats van één. + +CHARSET-onderhandeling is normaal op moderne PennMUSH, en daarom overleven namen met accenten de +reis. + +## Verwante servers + +PennMUSH, **TinyMUX**, **RhostMUSH** en **CobraMUSH** zijn vier servers met een gemeenschappelijke +voorouder en een gedeelde woordenschat — een bouwer die de ene kent, kan met moeite de softcode van +een andere lezen. Ze zijn niet compatibel: een database gaat niet zonder conversie van de een naar +de ander, en de functiebibliotheken verschillen op manieren die uitmaken. + +## SharpMUSH + +Een herimplementatie in .NET die op compatibiliteit met PennMUSH mikt is in ontwikkeling, door +dezelfde auteur als deze site. Niets op deze pagina is daaraan gemeten, en het heeft geen spellen in +de catalogus. diff --git a/content/reference/nl/codebase-rhostmush.md b/content/reference/nl/codebase-rhostmush.md new file mode 100644 index 0000000..4cf9f41 --- /dev/null +++ b/content/reference/nl/codebase-rhostmush.md @@ -0,0 +1,29 @@ +--- +kind: codebase +slug: rhostmush +title: RhostMUSH +summary: Een MUSH-server die bekendstaat om een diep rechtenmodel en een grote ingebouwde functieverzameling. Geen MSSP; beantwoordt een WHO vóór het inloggen. +codebase: RhostMUSH +home: https://github.com/RhostMUSH/trunk +see-also: codebases/pennmush +see-also: codebases/tinymux +see-also: codebases/cobramush +--- + +RhostMUSH is de vierde van de veelgebruikte servers die van TinyMUSH afstammen, en degene met het +meest uitgewerkte administratieve model: zijn rechten- en vlaggensysteem is aanzienlijk fijnmaziger +dan dat van zijn verwanten, en dat is de gebruikelijke reden dat een spel ervoor kiest. + +Zijn ingebouwde functiebibliotheek is groot, en softcode die voor Rhost geschreven is gaat vaak niet +schoon over naar PennMUSH of TinyMUX zonder dat de delen herschreven worden die functies gebruikten +die de andere niet hebben. + +## Hoe het er van buitenaf uitziet + +Geen MSSP. Een `WHO` vóór het inloggen die met een telling antwoordt. Er wordt over CHARSET +onderhandeld. + +Die combinatie — geen MSSP, een werkende `WHO` — is het handschrift van de MUSH-familie, en het is +de reden dat deze site het inlogscherm überhaupt peilt. Op grond van ons eigen onderzoek zijn de +MSSP- en de `WHO`-familie vrijwel disjunct: 28 codebases publiceren een telling via MSSP, zeven via +`WHO`, en slechts twee via beide. diff --git a/content/reference/nl/codebase-rom.md b/content/reference/nl/codebase-rom.md new file mode 100644 index 0000000..3168c92 --- /dev/null +++ b/content/reference/nl/codebase-rom.md @@ -0,0 +1,31 @@ +--- +kind: codebase +slug: rom +title: ROM +summary: De bekendste afstammeling van Merc, en de gevechtsengine waarop een groot deel van de MUD's uit de jaren negentig gebouwd is. +codebase: ROM +see-also: codebases/dikumud +see-also: codebases/smaug +see-also: protocols/mccp +--- + +ROM — *Rivers of MUD* — is een afgeleide van **Merc**, dat zelf een DikuMUD-afgeleide is, en het is +degene die is blijven hangen. Zijn gevechtsmodel, zijn vaardigheden- en spreukensysteem en zijn +areaformaat waren het beginpunt voor een enorm aantal spellen door de jaren negentig heen en daarna, +en ROM 2.4 in het bijzonder is een van de meest geforkte stukken broncode in de hobby. + +Zoals de rest van de Diku-lijn draagt het de oorspronkelijke crediteringseis mee, dus een spel +waarvan je de afstamming niet anders kunt vaststellen, noemt vaak Diku, Merc en ROM op zijn +inlogscherm. + +## Hoe het er van buitenaf uitziet + +MSSP, CHARSET en **MCCP2**, op het spel dat we gemeten hebben. + +ROM is de server waaraan dit project zijn eigen compressiebug heeft aangetoond. Onze peiling +onderhandelde MCCP2, de server begon correct te comprimeren, en de telnet-bibliotheek waar we van +afhangen pakte de stream nooit uit — dus kwam het verbindingsscherm aan als een muur van +vervangingstekens en hebben we dat even als de schuld van het spel vastgelegd. De payload liet zich +schoon uitpakken met een standaard zlib-aanroep, en dat maakte het onmiskenbaar. Het is upstream +hersteld; het verhaal staat op de pagina [MCCP](/reference/protocols/mccp), omdat het een goed +voorbeeld is van een gebrek dat er van buitenaf precies uitziet als een kapot spel. diff --git a/content/reference/nl/codebase-smaug.md b/content/reference/nl/codebase-smaug.md new file mode 100644 index 0000000..15fee74 --- /dev/null +++ b/content/reference/nl/codebase-smaug.md @@ -0,0 +1,27 @@ +--- +kind: codebase +slug: smaug +title: SMAUG +summary: Een Merc-afgeleide gebouwd voor grote thematische werelden, met goden, dimensies en een areaverzameling die vele malen groter is dan die van zijn ouder. +codebase: SMAUG +home: https://www.smaug.org/ +see-also: codebases/dikumud +see-also: codebases/rom +--- + +SMAUG — *Simulated Medieval Adventure multi-User Game* — kwam voort uit Realms of Despair en is een +Merc-afgeleide die groeide in plaats van vereenvoudigde. Waar ROM het gevecht van Merc strakker +maakte, voegde SMAUG toe: goden, dimensies, uitgebreidere area- en mobprogramma's, een veel grotere +standaardwereld, en een navenant grotere bronboom. + +Spellen die erop draaien zijn meestal de grote thematische, en verschillende draaien al +vijfentwintig jaar onafgebroken. + +## Hoe het er van buitenaf uitziet + +MSSP, beantwoord op verzoek, en verder niets onderhandeld op het spel dat we gemeten hebben. + +Let op voor wie tellingen vergelijkt: SMAUG-servers in ons onderzoek rapporteerden via MSSP en +beantwoordden geen `WHO` op het inlogscherm, dus het getal hier is de eigen MSSP-`PLAYERS` van het +spel — een bewering die de server doet, die alleen op haar woord geloofd wordt omdat we het niet +onafhankelijk konden meten. De spelpagina vermeldt van welke van de twee een gegeven cijfer kwam. diff --git a/content/reference/nl/codebase-tbamud.md b/content/reference/nl/codebase-tbamud.md new file mode 100644 index 0000000..7ff566e --- /dev/null +++ b/content/reference/nl/codebase-tbamud.md @@ -0,0 +1,25 @@ +--- +kind: codebase +slug: tbamud +title: tbaMUD +summary: De onderhouden voortzetting van CircleMUD, en het gebruikelijke antwoord op "ik wil vandaag een MUD in Diku-stijl beginnen". +codebase: tbaMUD +home: https://tbamud.com/ +see-also: codebases/circlemud +see-also: codebases/dikumud +--- + +tbaMUD — *The Builder Academy MUD* — pakte CircleMUD op waar de eigen ontwikkeling daarvan ophield +en is het sindsdien blijven bouwen, patchen en documenteren. Wil iemand een MUD met levels en +gevecht beginnen vanaf een basis die bekend goed is in plaats van vanaf een eigen fork, dan is dit +die basis. + +Het draagt de deugden van de Circle-codebase verder: leesbare C, een gedocumenteerd +areabestandsformaat, en een bouwershandleiding die ervan uitgaat dat je dit nog nooit gedaan hebt. +The Builder Academy zelf draait als een leerspel waar mensen erop leren bouwen. + +## Hoe het er van buitenaf uitziet + +MSSP, beantwoord op verzoek. Een gemeten nul op het spel dat we gepeild hebben — wat op deze site +een gevulde cel is en geen lege: we kwamen binnen, we lazen het getal, en het getal was nul. Dat is +een ander feit dan een spel dat we niet konden tellen, en die twee worden hier nooit samengevoegd. diff --git a/content/reference/nl/codebase-tinymush.md b/content/reference/nl/codebase-tinymush.md new file mode 100644 index 0000000..5cec23e --- /dev/null +++ b/content/reference/nl/codebase-tinymush.md @@ -0,0 +1,40 @@ +--- +kind: codebase +slug: tinymush +title: TinyMUSH +summary: De voorouder van de MUSH-lijn, met nog steeds draaiende spellen. Het leerde deze crawler dat zijn eigen onderhandelingsbytes het volgende commando kunnen breken dat hij stuurt. +codebase: TinyMUSH +home: https://github.com/TinyMUSH/TinyMUSH +see-also: codebases/pennmush +see-also: codebases/tinymux +see-also: mush-mud-muck-moo +--- + +TinyMUSH is waar de lijn van PennMUSH, TinyMUX, RhostMUSH en CobraMUSH allemaal van afstamt, en het +is nog steeds in gebruik. De ontwikkeling is stil, niet afwezig. + +## Hoe het er van buitenaf uitziet + +Geen MSSP. Een `WHO` vóór het inloggen die antwoordt met een zin in de vorm +`0 Players logged in, 22 record, no maximum.` + +## De bug die het in ons vond + +TinyMUSH is hier een alinea waard omdat het het spel is dat een gebrek in de eigen crawler van deze +site blootlegde, en de correctie is een goede illustratie van wat "gemeten" hoort te betekenen. + +Onze peiling las TinyMUSH wekenlang als *telling onbekend*. De gok die op papier stond was dat zijn +antwoord geen afsluitend regeleinde had. Dat heeft het wel. Van de lijn geplukt bleek de echte +oorzaak bij ons te liggen: **TinyMUSH ontleedt geen telnet op zijn inlogscherm**, dus de drie bytes +`IAC DO MSSP` die wij bij het verbinden sturen belanden in zijn invoerbuffer alsof iemand ze getypt +had. De volgende regel die het leest is niet `WHO` maar drie stuurbytes gevolgd door `WHO`, en dat +is geen commando dat het kent — dus toont het zijn verbindingsscherm opnieuw en zegt het niets over +spelers. + +De peiling stuurt nu een kaal regeleinde na het onderhandelen en gooit weg wat dat oplevert, want +die uitvoer is een reactie op bytes die *wij* verkozen te sturen en is daarom noch het +verbindingsscherm van het spel, noch zijn antwoord. TinyMUSH leest nu correct, en de peiling was in +een derde van de tijd klaar. + +Een gids die het niet nagekeken had, zou "dit spel meldt zijn spelers niet" gepubliceerd hebben +zolang het bestond, en die zin zou over ons gegaan zijn. diff --git a/content/reference/nl/codebase-tinymux.md b/content/reference/nl/codebase-tinymux.md new file mode 100644 index 0000000..7cb3498 --- /dev/null +++ b/content/reference/nl/codebase-tinymux.md @@ -0,0 +1,38 @@ +--- +kind: codebase +slug: tinymux +title: TinyMUX +summary: De andere grote MUSH-server. Softcode die dicht genoeg bij die van PennMUSH ligt om over te ruziën, helemaal geen MSSP, en een WHO vóór het inloggen die werkt. +codebase: TinyMUX +home: https://www.tinymux.org/ +see-also: codebases/pennmush +see-also: codebases/tinymush +see-also: codebases/rhostmush +see-also: mush-mud-muck-moo +--- + +TinyMUX is de tweede van de twee servers waarop de meeste gevestigde rollenspel-MUSHes draaien, en +voor veel spelers is de keuze tussen dit en PennMUSH een kwestie van welke de staf van hun spel het +eerst geleerd heeft. Versies lezen als `2.12` en dergelijke. + +Net als PennMUSH stamt het af van TinyMUSH, en de softcode ligt zo dicht bij elkaar dat een bouwer +die tussen de twee wisselt aan het vertalen is en niet aan het herleren. De verschillen zijn echt — +functiebibliotheken, een aantal hoeken van de parsing, de verzameling `@`-commando's — en het zijn +precies het soort dingen die het verplaatsen van een database tussen de twee eerder een project +maken dan een export. + +## Hoe het er van buitenaf uitziet + +**Geen MSSP.** TinyMUX biedt de optie helemaal niet aan, wat het samen met AresMUSH, MUCK, +RhostMUSH, CobraMUSH en TinyMUSH aan die kant van de hobby zet die een gids op MSSP alleen simpelweg +niet kan zien. Zijn spelerstelling komt van een `WHO` op het inlogscherm, die het met een kale +telling beantwoordt. + +Er wordt wél over CHARSET onderhandeld, en daarmee komt het op niet-ASCII-tekst beter uit de bus dan +de meeste van zijn verwanten. + +## Waar de tellingen vandaan komen + +Vergelijk je het getal van deze site voor een TinyMUX-spel met dat van een andere gids, houd er dan +rekening mee dat wij de `WHO` op het inlogscherm lezen en de meeste crawlers niet. Een gids die op +MSSP alleen gebouwd is, meldt deze spellen als hadden ze helemaal geen telling, of vermeldt ze niet. diff --git a/content/reference/nl/orientation-collaborative-roleplay.md b/content/reference/nl/orientation-collaborative-roleplay.md new file mode 100644 index 0000000..eeb8d16 --- /dev/null +++ b/content/reference/nl/orientation-collaborative-roleplay.md @@ -0,0 +1,66 @@ +--- +kind: orientation +slug: collaborative-roleplay +title: Je wilt gezamenlijk rollenspel +summary: Als je uit bent op het schrijven van scènes met andere mensen, is dit de korte weg — de codebases om naar te zoeken, een client die je niet tegenwerkt, en wat je op je eerste avond kunt verwachten. +see-also: mush-mud-muck-moo +see-also: connecting +see-also: codebases/pennmush +see-also: codebases/aresmush +see-also: codebases/evennia +see-also: clients/mudlet +--- + +Met *gezamenlijk rollenspel* bedoelen we: jij en andere mensen schrijven samen een scène, in +karakter, in proza, meestal in realtime. Niemand gooit initiatief tenzij het spel dat zegt. Dit is +het deel van de hobby dat achter het woord ‘MUD’ schuilgaat, en het leeft. + +## Zoek naar deze codebases + +Deze cultuur leeft in de TinyMUD-lijn, want die servers zijn gebouwd voor mensen die dingen maken en +niet voor mensen die dingen doden. + +- **[PennMUSH](/reference/codebases/pennmush)** en **[TinyMUX](/reference/codebases/tinymux)** — de + twee grote MUSH-servers. De meeste langlopende rollenspellen waar je van gehoord hebt draaien op + een van beide. +- **[AresMUSH](/reference/codebases/aresmush)** — modern, speciaal voor rollenspel gebouwd, met een + webfrontend en het vastleggen van scènes als eersterangsfuncties in plaats van als softcode die + iemand geschreven heeft. +- **[Evennia](/reference/codebases/evennia)** — een Python-framework in plaats van een afgerond spel. + Spellen die erop gebouwd zijn verschillen enorm; verschillende van de nieuwere rollenspellen zijn + Evennia. +- **[MUCK](/reference/codebases/muck)** — een eigen cultuur, sterk sociaal, en het kennen waard als + je op zoek bent naar de fandomwerelden. + +Je kunt de catalogus op elk hiervan filteren: elke codebasepagina linkt naar de spellen waarvan we +gemeten hebben dat ze erop draaien. + +## Een client die je niet tegenwerkt + +Je hebt er geen nodig — elk spel hier spreekt gewoon telnet, en je besturingssysteem heeft +waarschijnlijk al een client. Maar voor het schrijven van scènes doen drie dingen er meer toe dan +elders: + +1. **Een commandoregel die een lange alinea overleeft.** Je gaat poses van meerdere zinnen typen. +2. **Loggen.** Je wilt de scène achteraf hebben. +3. **UTF-8.** Namen hebben accenten. + +Zie het onderdeel [clients](/reference). Gebruik je een schermlezer, dan hebben de tabellen met +mogelijkheden daar een rij ervoor — we konden niet voor elke client een antwoord vaststellen, en waar +dat niet lukte, zegt de tabel *onbekend* in plaats van te gokken. + +## Hoe die eerste avond er echt uitziet + +De meeste rollenspellen hebben een **aanmeldprocedure**: je verbindt als gast, leest een paar +helpbestanden, en schrijft een personage uit dat de staf kan lezen. Dat kan een dag of twee weken +duren. Het is geen drempel om de drempel — het is hoe een spel een gedeelde setting samenhangend +houdt — maar het betekent wel dat ‘inloggen en spelen’ zelden de vorm ervan is. + +Spellen waar niemand is op het moment dat je kijkt zijn hier normaal, en het activiteitenraster op de +pagina van elk spel is nuttiger dan zijn actuele telling: een spel met vijftien mensen elke avond en +niemand om vier uur 's nachts is een gezond spel dat je op het verkeerde uur ziet. + +## Wat deze site je niet kan vertellen + +Of de mensen deugen. Of de staf eerlijk is. Of de setting er een is die jou bevalt. Wij meten +servers. Er is hier geen beoordeling en die komt er ook niet — dat is een bewust besluit, geen gat. diff --git a/content/reference/nl/orientation-connecting.md b/content/reference/nl/orientation-connecting.md new file mode 100644 index 0000000..c6285ea --- /dev/null +++ b/content/reference/nl/orientation-connecting.md @@ -0,0 +1,49 @@ +--- +kind: orientation +slug: connecting +title: Hoe je verbinding maakt +summary: Een host, een poort en telnet. Wat het adres op een spelpagina betekent en wat je ermee doet. +see-also: mush-mud-muck-moo +see-also: protocols/tls +see-also: protocols/charset +--- + +Elk spel dat hier vermeld staat antwoordt op een **host en een poort**, en het protocol eronder is +telnet — wat in de praktijk een kale TCP-verbinding betekent met een beetje optionele onderhandeling +erbovenop. + + telnet mush.pennmush.org 4201 + +Dat werkt, en op veel systemen staat het al geïnstalleerd. Het is ook een slechte manier om te +spelen: de `telnet` van het systeem heeft geen noemenswaardige controle over lokale echo, geen +logboek, geen geschiedenis, en hij verminkt alles boven ASCII. Het is het juiste gereedschap om te +controleren of een spel in de lucht is, en het verkeerde om er een avond in door te brengen. + +## Wat het adres op een spelpagina je vertelt + +Elke spelpagina somt de adressen op die we gemeten hebben, en markeert de adressen waarbij **TLS** +waargenomen is. Een spel met een TLS-poort is een spel waarmee je versleuteld verbinding kunt maken; +het poortnummer is meestal een ander dan dat van de gewone poort. + +Waar een spel meerdere poorten heeft, is dat vaak dezelfde wereld die op verschillende manieren +bereikt wordt en niet verschillende spellen. Wij vermelden wat we gemeten hebben en gokken niet welke +de officiële is. + +## Een client kiezen + +Het onderdeel [clients](/reference) heeft voor elk een pagina, met een tabel met mogelijkheden. De +drie dingen die het controleren waard zijn voordat je iets installeert: + +- **Doet hij UTF-8?** Als het spel niet alleen Engels is, kom je dit op je eerste avond tegen. +- **Doet hij TLS?** Alleen van belang als het spel het aanbiedt, maar verschillende doen dat + inmiddels. +- **Gebruik je een schermlezer: documenteert het project ondersteuning daarvoor?** Dit is de rij die + het vaakst ontbreekt in clientvergelijkingen, dus het is de eerste rij van de onze — en waar + niemand een antwoord heeft vastgesteld, staat er *onbekend*. + +## Als er niets antwoordt + +Een spel dat niet antwoordt is niet noodzakelijk weg. Spellen verhuizen van host, DNS verloopt, en +firewalls hebben hun eigen mening. Deze site bewaart elk spel dat ze ooit gemeten heeft — ook de +spellen die jaren geleden gestopt zijn met antwoorden — en blijft wekelijks aankloppen, dus het +[archief](/archive) is de plek om te kijken voordat je iets concludeert. diff --git a/content/reference/nl/orientation-families.md b/content/reference/nl/orientation-families.md new file mode 100644 index 0000000..df69738 --- /dev/null +++ b/content/reference/nl/orientation-families.md @@ -0,0 +1,82 @@ +--- +kind: orientation +slug: mush-mud-muck-moo +title: MUSH, MUD, MUCK, MOO — wat de woorden betekenen +summary: Vier woorden voor vier tradities, en geen ervan is een genre. Wat ze je werkelijk vertellen. +see-also: collaborative-roleplay +see-also: connecting +see-also: codebases/pennmush +see-also: codebases/aresmush +see-also: codebases/muck +see-also: codebases/moo +see-also: codebases/evennia +--- + +Elk van deze woorden noemt een **familie serversoftware**, niet een soort spel. Dat is het nuttigste +wat je erover kunt weten, en het is waarom ‘is dit een MUSH of een MUD?’ zo vaak slecht beantwoord +wordt: het eerlijke antwoord is meestal *allebei, en de vraag die je bedoelde ging over de cultuur*. + +## MUD + +De oudste term, en inmiddels de breedste. Het begon als *Multi-User Dungeon* — het spel van Bartle en +Trubshaw uit 1978 — en halverwege de jaren negentig was het het overkoepelende woord voor elke +tekstgebaseerde multiplayerwereld. + +In enge zin betekent het de **DikuMUD- en LPMud-lijnen**: servers gebouwd rond levels, gevechten, +uitrusting en een gebiedsbestand met kamers die een bouwer vooraf geschreven heeft. Zegt iemand ‘ik +speel een MUD’ en bedoelt diegene iets specifieks, dan is dit het meestal. + +De lijst kan je elke lijn apart tonen: [de DikuMUD-spellen](/games?lineage=DikuMUD) en +[de LPMud-spellen](/games?lineage=LPMud). + +## MUSH + +*Multi-User Shared Hallucination*, uit de TinyMUD-lijn. De bepalende eigenschap is niet het thema +maar de **softcode**: MUSH-servers leveren een programmeertaal mee die spelers vanuit het spel zelf +gebruiken, dus een speler met bouwrechten maakt kamers, objecten en gedrag zonder een bronbestand aan +te raken of iets opnieuw te starten. + +Die ene ontwerpbeslissing bracht de cultuur voort. MUSHes hebben doorgaans weinig geautomatiseerde +systemen en veel menselijke — plots die de staf draait, geschreven scènes, aanmeldprocedures — omdat +de mensen die spelen ook de mensen zijn die bouwen. + +PennMUSH, TinyMUSH, TinyMUX, RhostMUSH, CobraMUSH en AresMUSH zitten allemaal in deze lijn en geen +van alle zegt dat: MSSP kent geen waarde `MUSH` om te publiceren, en op PennMUSH na publiceert geen +van alle enige MSSP. Ze groeperen is dus iets wat wij doen en niet iets wat we lezen, en daarom is +[de MUSH-spellen](/games?lineage=MUSH) overal waar het voorkomt gemarkeerd als *afgeleid*. + +## MUCK + +Net als MUSH een afstammeling van TinyMUD, met eigen softcode (MUF, een Forth-achtige taal) en een +sterke traditie van sociale werelden en furry-fandomwerelden. Technisch dicht bij MUSH; cultureel +genoeg onderscheiden dat mensen die allebei spelen ze niet als hetzelfde zouden omschrijven — +[de MUCK-spellen](/games?lineage=MUCK). + +## MOO + +*MUD, Object-Oriented*. De zuiverste uitdrukking van het idee ‘het spel bewerkt zichzelf’: bijna +alles in een MOO is van binnenuit geschreven in de programmeertaal MOO, door de mensen die het +gebruiken. LambdaMOO is de voorvader, en MOO's zijn historisch even populair geweest in onderwijs en +onderzoek als in spellen. [De MOO-spellen](/games?lineage=MOO). + +## Wat moet je dan eigenlijk vragen? + +Drie vragen doen meer werk dan het woord van vier letters: + +1. **Zijn er gevechten, en zijn ze geautomatiseerd?** Dit scheidt de Diku/LP-lijn betrouwbaarder van + de TinyMUD-lijn dan welke naam ook. +2. **Wie bouwt er?** Alleen de staf, of iedereen met een bouwbit? +3. **Is het spel op afspraak of doorlopend?** Scènes op afspraak en rollenspel met poses, of + inloggen en gaan? + +De lijst op deze site kan een deel van de eerste vraag voor je beantwoorden: de **codebase** die we +voor een spel gemeten hebben vertelt je uit welke traditie zijn server komt, en het facet +**afstamming** is dat antwoord, filterbaar gemaakt. Het kan je niets over de cultuur vertellen, en +deze pagina zal niet doen alsof. + +Eén waarschuwing over dat facet, want deze pagina is waar iemand het zal tegenkomen. De codebase is +gemeten en de afstamming niet: die is *onze* groepering van wat een spel ons verteld heeft, gedragen +onder een eigen label — **afgeleid** — naast *gemeten* en *opgegeven*. Waar een codebase geen +onbetwiste ouder heeft, blijft hij buiten elke afstamming in plaats van onder de dichtstbijzijnde +geschaard te worden, en verschillende van die spellen zijn het in hun eigen woorden met ons eens, +door `FAMILY Custom` te publiceren. diff --git a/content/reference/nl/protocol-atcp.md b/content/reference/nl/protocol-atcp.md new file mode 100644 index 0000000..fd76391 --- /dev/null +++ b/content/reference/nl/protocol-atcp.md @@ -0,0 +1,31 @@ +--- +kind: protocol +slug: atcp +title: ATCP +summary: De voorloper van GMCP. Out-of-bandgegevens met een losser payloadformaat, grotendeels achterhaald, en nog altijd onderhandeld door servers die het nooit verwijderd hebben. +protocol: ATCP +see-also: protocols/gmcp +see-also: protocols/msdp +see-also: clients/mudlet +--- + +ATCP — het Achaea Telnet Client Protocol — is telnet-optie 200, en het is de plek waar het idee om +gestructureerde gegevens naast MUD-tekst te sturen voor het eerst breed werd uitgerold. Een server +stuurt een modulenaam en een payload; de client routeert die. + +Het payloadformaat is losser dan de JSON van [GMCP](/reference/protocols/gmcp), en dat is in wezen +waarom GMCP het vervangen heeft. Clients die ATCP ondersteunen documenteren het tegenwoordig +doorgaans als verouderd en verwijzen je door naar GMCP. + +## Waarom het er nog is + +Omdat er niets stukgaat door het aan te laten staan. Een server die ATCP in 2008 implementeerde en er +in 2014 GMCP bij deed, onderhandelt meestal nog over allebei, en een client die allebei ondersteunt +neemt wat hem aangeboden wordt. + +Voor een nieuwe implementatie is er geen reden om het te kiezen. + +## Wat we meten + +Servers die telnet-optie 200 aanboden in een handshake die we waargenomen hebben. Een laag cijfer is +hier te verwachten en gaat over ouderdom en verder over niets. diff --git a/content/reference/nl/protocol-charset.md b/content/reference/nl/protocol-charset.md new file mode 100644 index 0000000..6bea104 --- /dev/null +++ b/content/reference/nl/protocol-charset.md @@ -0,0 +1,39 @@ +--- +kind: protocol +slug: charset +title: CHARSET +summary: De telnet-optie uit RFC 2066 om een codering af te spreken. De reden dat namen met accenten in een spel de reis overleven, en de bron van een paar subtiele fouten wanneer hij ontbreekt. +protocol: CHARSET +home: https://www.rfc-editor.org/rfc/rfc2066 +see-also: protocols/ttype +see-also: connecting +see-also: codebases/tinymux +--- + +CHARSET is telnet-optie 42, gespecificeerd in RFC 2066. De ene kant biedt een lijst met tekensets +aan, de andere kiest er een, en daarna zijn beide het eens over hoe bytes op tekens worden afgebeeld. + +In de praktijk komt de onderhandeling uit op **UTF-8** of vindt ze helemaal niet plaats. De +MUSH-familie onderhandelt er merkbaar vaker over dan de MUD-familie — TinyMUX, RhostMUSH en PennMUSH +doen het alle drie — en dat past bij een bevolking die proza schrijft met namen erin. + +## Wat er gebeurt zonder + +Een client moet raden, en de gebruikelijke gok is ASCII of Latin-1. Gok ASCII en elke byte boven 0x7F +wordt een vraagteken; gok Latin-1 op een UTF-8-server en elk letterteken met een accent wordt twee +leestekens. Beide fouten zien eruit als de schuld van het spel en zijn dat niet. + +Voor een crawler bijt dit op één bepaalde plek. Onze eigen telnet-bibliotheek zet haar huidige +codering standaard op ASCII, en die standaardwaarde is niet onschuldig — het is waarmee elke byte +gedecodeerd wordt, voor elke server die nooit over CHARSET onderhandelt, en dat zijn de meeste. +Daarom stellen we hem bewust zelf in. + +## De ene plek waar CHARSET niet komt + +MSSP-veldnamen en -waarden worden als ASCII gedecodeerd, ongeacht waar CHARSET op uitkwam, want een +subonderhandeling is een commando en geen tekst, en de specificatie beperkt CHARSET tot tekst. Dat is +verdedigbaar conform en het is verliesgevend: een spel waarvan de MSSP-`NAME` `Café Noir` is, meldt +`Caf? Noir`, en de oorspronkelijke bytes zijn weg voordat iets wat wij in de hand hebben ze ziet. + +Zie je op deze site een verminkt teken in een opgegeven veld en niet in de uitvoer van het spel zelf, +dan is dat de reden, en van onze kant valt het niet meer te herstellen. diff --git a/content/reference/nl/protocol-gmcp.md b/content/reference/nl/protocol-gmcp.md new file mode 100644 index 0000000..27f741f --- /dev/null +++ b/content/reference/nl/protocol-gmcp.md @@ -0,0 +1,44 @@ +--- +kind: protocol +slug: gmcp +title: GMCP +summary: Het Generic Mud Communication Protocol — gestructureerde JSON-berichten naast de tekst, en het out-of-bandkanaal waar de meeste moderne clients tegenaan bouwen. +protocol: GMCP +home: https://www.mudhalla.net/tintin/protocols/gmcp/ +see-also: protocols/msdp +see-also: protocols/atcp +see-also: clients/mudlet +--- + +GMCP is telnet-optie 201. Zodra erover onderhandeld is, kan de server **gestructureerde gegevens out +of band** sturen: een pakketnaam en een JSON-payload, die in dezelfde stroom aankomen als de tekst +maar er geen deel van uitmaken. + +`Char.Vitals { "hp": 412, "maxhp": 500 }` is het klassieke voorbeeld. Een client kan daar een +levensbalk mee aansturen zonder het proza af te struinen op getallen, en dat is precies de bedoeling +— een statusweergave die op patroonherkenning in de tekst gebouwd is, breekt op de dag dat een spel +zijn prompt verandert, en een die op GMCP gebouwd is niet. + +De naamruimte van de pakketten berust op gewoonte en niet op een standaard. `Char`, `Room`, `Comm` en +`Client` zijn breed in gebruik; daarbuiten verzinnen spellen wat ze nodig hebben, en een client moet +meestal verteld worden wat een bepaald spel stuurt. + +## Waarom het ATCP verdrongen heeft + +GMCP is de opvolger van [ATCP](/reference/protocols/atcp), dat hetzelfde werk deed met een losser +payloadformaat. JSON was de verbetering, en halverwege de jaren 2010 was de overstap grotendeels +voltooid. Een spel dat allebei ondersteunt is niet ongewoon; een nieuw spel dat alleen ATCP +ondersteunt zou dat wel zijn. + +## Wat we meten + +Een spel telt hier mee wanneer **zijn server GMCP aanbood in een handshake die we waargenomen +hebben**. Dat is een andere bewering dan dat de MSSP van een spel `GMCP 1` zegt, waar de meeste +protocoltabellen in deze hobby op gebouwd zijn, en die twee wijken geregeld van elkaar af. + +Eén kanttekening bij de meting, uit onze eigen geschiedenis: een tijdlang konden we GMCP niet zien op +servers die ook over [MCCP](/reference/protocols/mccp) onderhandelden, doordat onze +telnet-bibliotheek over compressie onderhandelde zonder de stroom uit te pakken, en alles na het +compressiemarkeerpunt was voor ons ruis. Van minstens één server in ons onderzoek bleek dat hij al +die tijd GMCP sprak. Als een cijfer op deze pagina laag lijkt voor een familie die je goed kent, is +dat soort defect het eerste om te verdenken — bij ons, niet bij hen. diff --git a/content/reference/nl/protocol-mccp.md b/content/reference/nl/protocol-mccp.md new file mode 100644 index 0000000..fb1e400 --- /dev/null +++ b/content/reference/nl/protocol-mccp.md @@ -0,0 +1,44 @@ +--- +kind: protocol +slug: mccp +title: MCCP +summary: Compressie van de stroom. Goedkoop, breed uitgerold, en het protocol dat de leerzaamste bug in de geschiedenis van dit project opleverde. +protocol: MCCP +home: https://www.mudhalla.net/tintin/protocols/mccp/ +see-also: codebases/rom +see-also: codebases/dikumud +see-also: protocols/gmcp +--- + +MCCP comprimeert de stroom van server naar client met zlib. Versie 1 is telnet-optie 85 en is +feitelijk historisch; **versie 2** is optie 86 en is waarover moderne servers onderhandelen. Nadat de +server `IAC SB MCCP2 IAC SE` gestuurd heeft, hoort elke byte die volgt bij één doorlopende +zlib-stroom. + +Het is een echte besparing op een tekstprotocol — MUD-uitvoer laat zich uitzonderlijk goed +comprimeren — en het is gangbaar in de Diku- en LP-families, waar ruwweg een derde van de codebases +in ons onderzoek erover onderhandelt. + +## Hoe het misgaat, en waarom dat hier uitmaakt + +Een client die over MCCP2 onderhandelt en de stroom vervolgens niet uitpakt, ontvangt **binaire +rommel vanaf het compressiemarkeerpunt**. Geen foutmelding, geen verbroken verbinding: het +verbindingsscherm komt binnen als een muur van vervangingstekens, en alles daarna — het +`WHO`-antwoord, elke latere MSSP, de hele sessie — is verloren. + +Dit is niet hypothetisch. Onze eigen telnet-bibliotheek deed precies dat. Ze onderhandelde over de +optie, vuurde haar callback voor ‘compressie ingeschakeld’ af, en pakte geen enkele byte uit. De +payload liet zich met een gewone zlib-aanroep zonder problemen decomprimeren, en dat maakte +ondubbelzinnig duidelijk dat de servers gelijk hadden en wij niet. Dertien van de achtendertig +codebases in ons onderzoek waren geraakt, en zolang het duurde konden we niet waarnemen waarover die +servers *na* het begin van de compressie onderhandelden — dus ons beeld van hun mogelijkheden stelde +ze te laag voor. + +Het is upstream opgelost. Een vervolgdefect — de inflater die per leesbewerking opnieuw wordt +aangemaakt in plaats van voor de duur van de verbinding bewaard te blijven, wat halverwege een groot +verbindingsscherm misgaat — is gemeld en staat open, en treft de staart van de grootste schermen. + +Twee dingen die een lezer hieruit mee moet nemen. **Een protocolcijfer op deze pagina is net zozeer +een meting van onze crawler als van de hobby**, en waar we weten dat het fout is geweest, zeggen we +dat. En schrijf je een client: over MCCP onderhandelen is makkelijk, en het correct uitpakken is waar +het werk zit. diff --git a/content/reference/nl/protocol-msdp.md b/content/reference/nl/protocol-msdp.md new file mode 100644 index 0000000..fc99e17 --- /dev/null +++ b/content/reference/nl/protocol-msdp.md @@ -0,0 +1,31 @@ +--- +kind: protocol +slug: msdp +title: MSDP +summary: Het Mud Server Data Protocol — hetzelfde werk als GMCP, gedaan met een compacte binaire codering en een ontdekkingsmechanisme dat GMCP niet heeft. +protocol: MSDP +home: https://www.mudhalla.net/tintin/protocols/msdp/ +see-also: protocols/gmcp +see-also: clients/tintin +see-also: clients/blightmud +--- + +MSDP is telnet-optie 69, en het lost hetzelfde probleem op als [GMCP](/reference/protocols/gmcp): +gestructureerde gegevens naast de tekst sturen, zodat een client geen proza hoeft af te struinen op +getallen. + +De verschillen zijn er twee. De codering van MSDP is **binair en compact** — variabelen en waarden +worden met losse stuurbytes gemarkeerd in plaats van in JSON verpakt — en MSDP definieert een gesprek +voor **ontdekking**: een client kan met `LIST` naar `COMMANDS`, `REPORTABLE_VARIABLES` enzovoort +vragen, en te horen krijgen wat een bepaald spel ondersteunt. GMCP heeft daar geen equivalent voor, +en daarom moet een GMCP-client meestal per spel ingesteld worden. + +In de praktijk heeft GMCP het gewonnen op adoptie en houdt MSDP stand in de servers en clients die +het geïmplementeerd hebben, vaak naast GMCP. + +## Wat we meten + +Een spel telt hier mee wanneer zijn server MSDP aanbood in een handshake die we waargenomen hebben. +Zoals bij elk cijfer in dit onderdeel is dat een positieve waarneming, en de rest is niet het +tegendeel ervan — een spel dat niet meetelt, implementeert MSDP misschien niet, of we hebben zijn +handshake simpelweg nog niet gelezen. diff --git a/content/reference/nl/protocol-msp.md b/content/reference/nl/protocol-msp.md new file mode 100644 index 0000000..9e2e338 --- /dev/null +++ b/content/reference/nl/protocol-msp.md @@ -0,0 +1,41 @@ +--- +kind: protocol +slug: msp +title: MSP +summary: Het MUD Sound Protocol — de server noemt een geluidsbestand en de client speelt het af. Oud, eenvoudig, en makkelijk te verwarren met twee andere dingen. +protocol: MSP +home: https://www.zuggsoft.com/zmud/msp.htm +see-also: protocols/mxp +see-also: clients/vipmud +--- + +Met MSP kan een server een client vragen een geluid af te spelen: een instructie tussen haakjes die +een bestand noemt, een volume, een aantal herhalingen en een URL om het vandaan te halen als de +client het niet heeft. Het onderhandelt op telnet-optie 90, en het kan ook in band in de tekststroom +gestuurd worden door servers die nergens over onderhandelen. + +Het is werkelijk oud en werkelijk nog in gebruik — omgevingsgeluid in een tekstspel doet meer dan het +klinkt, en voor spelers die de geluidssignalen van een client gebruiken in plaats van het beeld is +het meer dan versiering. + +## Drie dingen die het niet is + +De clienttabellen in dit onderdeel moesten hier voorzichtig zijn, en het is de moeite waard op te +schrijven waarom: + +- **MCMP** — het Mud Client Media Protocol — is een ander protocol dat soortgelijk werk doet. + Minstens één client implementeert MCMP en geen MSP, en het ene voor het andere lezen zou een + bewering in een tabel zetten die niemand gedaan heeft. +- **De eigen scriptaanroep ‘speel een geluid af’ van een client** is geen MSP. Die speelt een lokaal + bestand af wanneer een script dat zegt; bij MSP vertelt een server een client wat hij moet + afspelen. +- **Ondersteuning via een meegeleverde plug-in is het waard om als zodanig te vermelden.** Bij één + client komt de MSP-ondersteuning als een plug-in die uitdrukkelijk geen telnet-onderhandeling doet, + wat werkt bij servers die MSP in band sturen en niet bij servers die verwachten dat erover + onderhandeld wordt. + +## Wat we meten + +Servers die telnet-optie 90 aanbieden. Doordat MSP vaak in band gestuurd wordt zonder onderhandeling, +stelt dit cijfer de uitrol te laag voor met een hoeveelheid die we niet kunnen schatten — en dat is +een beperking van wat een handshake kan zien, geen bevinding over het protocol. diff --git a/content/reference/nl/protocol-mssp.md b/content/reference/nl/protocol-mssp.md new file mode 100644 index 0000000..014aa77 --- /dev/null +++ b/content/reference/nl/protocol-mssp.md @@ -0,0 +1,60 @@ +--- +kind: protocol +slug: mssp +title: MSSP +summary: Het Mud Server Status Protocol — hoe een spel een crawler over zichzelf vertelt. Alles wat het meldt is opgegeven, niet gemeten, en deze site houdt die twee uit elkaar. +protocol: MSSP +home: https://www.mudhalla.net/tintin/protocols/mssp/ +see-also: protocols/gmcp +see-also: codebases/dikumud +see-also: codebases/pennmush +--- + +MSSP is telnet-optie 70. Een crawler stuurt `IAC DO MSSP`; een server die het ondersteunt antwoordt +met een tabel van naam/waarde-paren die hem beschrijft — naam, spelerstelling, codebase, uptime, +hostnaam, poort, genre, en wat hij verder ook wil publiceren. + +Het komt het dichtst in de buurt van wat deze hobby aan een machineleesbare gidsvermelding heeft, en +het is de reden dat verschillende gidsen überhaupt bestaan. + +## Alles in een MSSP-rapport is een bewering + +Dit is het punt waarop deze site van elke gevestigde gids verschilt. Een MSSP-rapport is het spel dat +je over zichzelf *vertelt*. `GMCP 1` in een MSSP-tabel betekent dat iemand `1` in een +configuratiebestand getypt heeft, misschien wel in 2011. Het is geen bewijs dat de server GMCP +aanbiedt, en de twee wijken vaak genoeg van elkaar af om interessant te zijn. + +Feiten die uit MSSP komen krijgen hier dus het label **opgegeven**, en waar we hetzelfde feit kunnen +meten — een mogelijkheid, door te kijken of er werkelijk over de optie onderhandeld wordt — worden +beide getoond, naast elkaar, elk met een ouderdom erbij. Een spel waarvan de MSSP al zes jaar GMCP +opgeeft en het nog nooit één keer in een handshake aangeboden heeft, is een feit dat het weten waard +is, en nergens anders is het te vinden. + +Het ene veld dat we bewust in het geheel niet laten meetellen is `CREATED`. Het is één met de hand +ingetypte regel, en het ergens voor laten meetellen zou dat iets triviaal manipuleerbaar maken. + +## Wie het beantwoordt + +MSSP is het antwoord van **Diku en LP**. In ons eigen onderzoek onder 38 codebases publiceerden er 28 +een spelerstelling via MSSP en zeven via een `WHO` op het inlogscherm, en maar twee deden allebei — +de twee families zijn vrijwel disjunct. AresMUSH, TinyMUX, MUCK, RhostMUSH, CobraMUSH en TinyMUSH +bieden helemaal geen MSSP aan. + +Dat is het empirische argument om vier lagen te peilen in plaats van één: **een crawler die alleen op +MSSP gebouwd is, kan het grootste deel van de MUSH-familie niet zien**, en dat is een groot deel van +de hobby en het merendeel van het beoogde publiek van deze site. + +## Vragen, niet afwachten + +Heel veel servers die MSSP volledig ondersteunen zullen het nooit uit zichzelf aanbieden — ze +beantwoorden `IAC DO MSSP` en zeggen verder niets. Een crawler die opent met `IAC WILL NAWS` en dan +wacht, meldt die spellen dus als spellen die niets publiceren, en dat is een bewering over de server +die voortkomt uit het zwijgen van de crawler zelf. Wij sturen `IAC DO MSSP` bij het verbinden. + +## De vorm in platte tekst + +Er bestaat een oudere variant waarin een client bij het inlogscherm letterlijk de regel +`MSSP-REQUEST` stuurt. We hebben het gemeten: van de twintig geprobeerde spellen antwoordden er drie +— en alle drie beantwoordden ook telnet-optie 70, dus het bereikte niets wat de optie niet al +bereikte. Acht servers lazen het verzoek als een **personagenaam** en zeiden dat ook, waarmee een van +de inlogpogingen opging die een vreemde krijgt. Wij sturen het niet. diff --git a/content/reference/nl/protocol-mxp.md b/content/reference/nl/protocol-mxp.md new file mode 100644 index 0000000..b3a94a3 --- /dev/null +++ b/content/reference/nl/protocol-mxp.md @@ -0,0 +1,41 @@ +--- +kind: protocol +slug: mxp +title: MXP +summary: Het MUD eXtension Protocol — HTML-achtige opmaak in de tekststroom, goed voor klikbare links, afbeeldingen en formulieren. Breed gespecificeerd, ongelijkmatig geïmplementeerd. +protocol: MXP +home: https://www.zuggsoft.com/zmud/mxp.htm +see-also: protocols/pueblo +see-also: clients/mushclient +see-also: clients/mudlet +--- + +MXP bouwt een kleine, HTML-achtige opmaaktaal in de tekst die een server stuurt: `` voor een +klikbaar commando, `` voor een link, elementen voor kleur en lettertype, en een mechanisme +waarmee een server eigen tags kan definiëren. Het onderhandelt op telnet-optie 91. + +Het ontwerpprobleem zit er inherent in en is interessant: de opmaak reist in dezelfde stroom als de +tekst, dus een server moet oppassen met tekst die er *uitziet* als opmaak, en een client moet +oppassen met wat hij weergeeft. Precies daarom definieert MXP beveiligingsniveaus — een tag die +binnenkomt in een regel chat van een andere speler is niet hetzelfde als een tag die de server zelf +uitgestuurd heeft. + +## Klikbaarheid is waarom mensen het willen + +Waar MXP in de praktijk vooral voor gebruikt wordt, is `north` en namen van voorwerpen veranderen in +dingen waarop je kunt klikken. Voor een nieuwe speler scheelt dat aanzienlijk, en daarom blijft het +protocol geïmplementeerd worden ondanks zijn complexiteit. + +## Pueblo is die andere + +[Pueblo](/reference/protocols/pueblo) is ouder dan MXP en doet soortgelijk werk met een andere +aanpak, die letterlijker de vorm van HTML heeft. Een client die het ene ondersteunt, ondersteunt het +andere vaak niet, en de twee zijn makkelijk te verwarren bij het lezen van een functielijst — een +fout waarmee we in de clienttabellen in dit onderdeel hebben moeten oppassen. + +## Wat we meten + +Servers die telnet-optie 91 aanboden in een handshake die we waargenomen hebben. Er wordt minder vaak +over MXP onderhandeld dan over de out-of-bandprotocollen, deels doordat veel van zijn waarde +gerealiseerd wordt door servers die de opmaak gewoon uitsturen en hopen, zonder ergens over te +onderhandelen — en dat kunnen wij niet zien. diff --git a/content/reference/nl/protocol-pueblo.md b/content/reference/nl/protocol-pueblo.md new file mode 100644 index 0000000..6f363f6 --- /dev/null +++ b/content/reference/nl/protocol-pueblo.md @@ -0,0 +1,32 @@ +--- +kind: protocol +slug: pueblo +title: Pueblo +summary: Het oudere schema voor HTML in een MUD, afkomstig uit de gelijknamige client. Nog altijd ondersteund door clients aan de MUSH-kant, en stelselmatig verward met MXP. +protocol: PUEBLO +home: https://pueblo.sourceforge.net/ +see-also: protocols/mxp +see-also: clients/beipmu +--- + +Pueblo kwam halverwege de jaren negentig voort uit de gelijknamige client en koos een directe aanpak +om MUD-tekst rijker te maken: laat de server **HTML** sturen, en laat de client die weergeven. Een +server kondigt Pueblo-ondersteuning aan in een regel bij het verbinden; de client antwoordt, en vanaf +dat moment mag de stroom opmaak bevatten. + +Het bereikte de MUSH-kant van de hobby meer dan de MUD-kant, en MUSH-servers die het ondersteunen +doen dat over het algemeen nog steeds. + +## Niet MXP + +[MXP](/reference/protocols/mxp) is het latere schema en het breder geïmplementeerde. Ze doen +soortgelijk werk en zijn niet uitwisselbaar, en de Pueblo-ondersteuning van een client lezen als +MXP-ondersteuning — of andersom — is de allermakkelijkste fout bij het samenstellen van een +clientvergelijking. Daarom houden de clientpagina's in dit onderdeel ze uit elkaar, en waar een +project het ene documenteert en het andere niet, zegt het andere *onbekend*. + +## Wat we meten + +De handshake van Pueblo is geen telnet-optie in de gebruikelijke zin, dus wat we waarnemen is smaller +dan bij de onderhandelde protocollen, en een laag cijfer hier moet gelezen worden als een uitspraak +over ons zicht erop en niet over de uitrol. diff --git a/content/reference/nl/protocol-tls.md b/content/reference/nl/protocol-tls.md new file mode 100644 index 0000000..aa5ea18 --- /dev/null +++ b/content/reference/nl/protocol-tls.md @@ -0,0 +1,37 @@ +--- +kind: protocol +slug: tls +title: TLS +summary: Versleutelde verbindingen. Meestal een aparte poort in plaats van een onderhandelde opwaardering, en de enige mogelijkheid op deze site die we vaststellen door verbinding te maken in plaats van door te vragen. +protocol: TLS +see-also: connecting +see-also: protocols/charset +see-also: clients/potato +--- + +Telnet is platte tekst. Alles wat je naar een MU\* stuurt — je wachtwoord inbegrepen — gaat over het +netwerk, leesbaar voor alles wat onderweg meekijkt, tenzij het spel TLS aanbiedt. + +In deze hobby betekent TLS bijna altijd **een tweede poort die vanaf de eerste byte TLS spreekt**, en +niet een opwaardering in band. Een spel met een gewone poort op 4201 en een TLS-poort op 4202 is de +gebruikelijke vorm. Er bestaat een onderhandelde variant, en die is zeldzaam genoeg dat de +documentatie van minstens één client uitdrukkelijk zegt dat hij niet ondersteund wordt. + +## Waarom de spelpagina's dit apart markeren + +TLS is de enige mogelijkheid op deze site die vastgesteld wordt door het te *doen*: een adres wordt +als TLS gemarkeerd omdat we er een TLS-handshake mee voltooid hebben. Er komt geen vragen aan te pas +en er is geen veld om iets in op te geven, wat het de zuiverste meting in de catalogus maakt. + +Daarom worden de TLS-poort en de gewone poort van een spel ook als aparte adressen vermeld en niet +samengevoegd. Het zijn verschillende metingen van verschillende dingen. + +## Praktisch advies + +Biedt een spel dat je speelt een TLS-poort aan, gebruik hem dan. Doet het dat niet en vind je het +belangrijk, vraag er dan om — het is weinig werk voor een beheerder, en dat het niet overal is, komt +vooral doordat niemand erom gevraagd heeft en niet doordat iemand bezwaar maakt. + +Controleer of je client het ondersteunt voordat je erop vertrouwt. Verschillende in het onderdeel +[clients](/reference) doen dat; minstens één documenteert in plaats daarvan een omweg met een extern +`stunnel`-proces, wat werkt en meer opzetwerk is dan de meeste mensen zullen doen. diff --git a/content/reference/nl/protocol-ttype.md b/content/reference/nl/protocol-ttype.md new file mode 100644 index 0000000..10787c9 --- /dev/null +++ b/content/reference/nl/protocol-ttype.md @@ -0,0 +1,46 @@ +--- +kind: protocol +slug: ttype +title: TTYPE en MTTS +summary: Hoe een client een server vertelt wat hij is en wat hij kan — inclusief, als de client dat wil zeggen, dat er een schermlezer in gebruik is. +protocol: TTYPE +home: https://www.mudhalla.net/tintin/protocols/mtts/ +see-also: protocols/charset +see-also: clients/tintin +see-also: clients/blightmud +--- + +TTYPE is telnet-optie 24, uit RFC 1091: de server vraagt de client welke terminal hij is, en de +client antwoordt. Historisch was het antwoord `VT100` of `ANSI`. + +**MTTS** — de Mud Terminal Type Standard — legt daar een afspraak overheen. Een client antwoordt drie +keer: zijn naam, zijn terminaltype, en dan `MTTS `, waarbij de bits mogelijkheden opgeven. +256 kleuren, true colour, UTF-8, MNES, MSP over out-of-band — en, opvallend genoeg, +**`MTTS_SCREEN_READER`**. + +## Het schermlezerbit + +Bij die laatste is het de moeite waard even stil te staan, want het is de enige plek in de +protocolstapel van deze hobby waar toegankelijkheid een eersterangsbegrip is. + +Een client die het zet, vertelt de server dat er een schermlezer in gebruik is, en een server die dat +opmerkt kan zich aanpassen: ASCII-kunst onderdrukken, de decoratieve kaderlijnen rond een +kamerbeschrijving weglaten, een tabel anders opmaken. Zowel [TinTin++](/reference/clients/tintin) als +[Blightmud](/reference/clients/blightmud) adverteert het, en [Mudlet](/reference/clients/mudlet) +heeft er een instelling voor. + +Of een bepaald spel er iets mee doet is een andere vraag, en niet een die deze site kan meten — we +kunnen een server niet vragen wat hij anders zou doen. + +## Wat een crawler hier verplicht is + +Een crawler maakt zichzelf via TTYPE bekend, en dat hoort ook. De onze doet dat, met een URL met +informatie, zodat een beheerder die zijn logboeken leest kan achterhalen wie er verbinding met zijn +spel gemaakt heeft en hoe hij ons kan vragen te stoppen. Een crawler die `ANSI` antwoordt en verder +niets, is anoniem van opzet, en daar is geen goede reden voor. + +## Wat we meten + +Servers die met ons over TTYPE onderhandeld hebben. Let op: dit is een van de weinige opties waarbij +*wij* de partij zijn die gevraagd wordt, dus een cijfer hier is een telling van servers die de moeite +namen te vragen. diff --git a/content/reference/zh-Hans/client-atlantis.md b/content/reference/zh-Hans/client-atlantis.md new file mode 100644 index 0000000..e77344c --- /dev/null +++ b/content/reference/zh-Hans/client-atlantis.md @@ -0,0 +1,40 @@ +--- +kind: client +slug: atlantis +title: Atlantis +summary: 一个只跑在 macOS 上的客户端,活得久,在 beta 里也待得久。它的脚本功能有文档说明已经不能用了,这是本节里唯一一个诚实的“否”。 +home: https://www.riverdark.net/atlantis/ +platform: macOS +capability: screen reader | unknown | +capability: TLS | yes | https://www.riverdark.net/atlantis/history.php +capability: UTF-8 | yes | https://www.riverdark.net/atlantis/history.php +capability: MCCP | yes | https://www.riverdark.net/atlantis/history.php +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | no | https://www.riverdark.net/atlantis/ +see-also: clients/mudlet +see-also: protocols/charset +--- + +Atlantis 是一个原生 macOS 客户端,从 Mac OS X 10.3 起就在了,并在 Catalina 时期更新到了 64 位。它 +能处理 RFC 2066 字符集协商和 Unicode,这比它的年纪所暗示的要好,而且它支持 MCCP 和 SSL。 + +## 本节里唯一的那个“否” + +它的脚本功能是 Perl,经由 CamelBones 桥接实现,而项目自己的主页说它已经不能用了——Apple 对 Perl 的 +处理方式变了,而那个库的作者几年前去世了。这是一处*有出处的缺失*,与未知是不同的东西,也是整个客 +户端部分里唯一带着这种答案的格子。别的每一处,诚实的答案都是我们无法确立。 + +## 我们无法确立的一切 + +它的版本历史完整而公开,其中提到了 **MCCP**、**SSL** 和**字符集协商**——而从未提到 GMCP、MSDP、 +ATCP 或 MSP。MXP 出现过一次,作为打算放进 1.0.0 之后某个版本的东西,而那个版本还没有到来。 + +脚本 API 里有一个 Perl 的 `Atlantis::Speak()` 调用,很容易把它读成屏幕阅读器支持。它不是:它是一个 +脚本化的文本转语音调用,而它所在的那套脚本系统,项目自己说是不能用的。VoiceOver、“accessible”和 +“screen reader”在主页、下载页、完整版本历史和已归档的用户指南上都不曾出现。 + +当前可下载的是 0.9.9.8,名义上仍是 beta,而站上任何地方都没有公布发布日期。 diff --git a/content/reference/zh-Hans/client-beipmu.md b/content/reference/zh-Hans/client-beipmu.md new file mode 100644 index 0000000..da43ba8 --- /dev/null +++ b/content/reference/zh-Hans/client-beipmu.md @@ -0,0 +1,42 @@ +--- +kind: client +slug: beipmu +title: BeipMU +summary: 一个瞄准这个爱好中 MUSH 那一边的 Windows 客户端,输出窗口有屏幕阅读器支持,走的是 Pueblo 而不是 MXP。 +home: https://beipdev.github.io/BeipMU/ +platform: Windows +capability: screen reader | yes | https://github.com/BeipDev/BeipMU/blob/master/Assets/Changes.txt +capability: TLS | yes | https://beipdev.github.io/BeipMU/ +capability: UTF-8 | yes | https://beipdev.github.io/BeipMU/ +capability: MCCP | unknown | +capability: GMCP | yes | https://github.com/BeipDev/BeipMU/blob/master/Documentation/GMCP.md +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://beipdev.github.io/BeipMU/ +see-also: clients/mushclient +see-also: clients/potato +see-also: collaborative-roleplay +--- + +BeipMU 是一个 MIT 许可的 Windows 客户端,发布活跃,也是少数几个在设计时想的是 MUSH 式玩法而不是战 +斗 MUD 的客户端之一——多个输入窗口、spawn 窗口,以及一个预期会遇到长段落的文本引擎。脚本默认是 +JavaScript,也可以用其他 ActiveScript 引擎。 + +## 无障碍 + +输出窗口实现了 Windows 的 `IAccessible` 接口,这是有意加进去的,作为迈向视障玩家可用性的一步,另外 +还有一个用于文本转语音的 **Speak** 触发动作。任何地方都没有点名某个具体的屏幕阅读器,文档里也没有 +无障碍相关的章节。 + +如果你要去翻,有一点需要留神:项目自己的文档里有一页至今仍写着 BeipMU 无法使用语音合成。那一页过时 +了——变更日志和维护者本人在 issue 里的留言都晚于它。 + +## 关于这个客户端的两个容易犯的错 + +**BeipMU 实现的是 MCMP,不是 MSP。**它们是两个名字相似、用途也相似的不同协议,把其中一个读成另一 +个,会在这张表里写下一个谁也没有做过的主张。所以 MSP 那一行写的是未知。 + +**它支持的是 Pueblo,不是 MXP。**Pueblo 是较早的那套“在 MUD 里用 HTML”的方案,MXP 是较晚的那套; +BeipMU 有文档说明基本的 Pueblo 样式和可点击链接。MXP 则无论有无都未能确立。 diff --git a/content/reference/zh-Hans/client-blightmud.md b/content/reference/zh-Hans/client-blightmud.md new file mode 100644 index 0000000..ae87acf --- /dev/null +++ b/content/reference/zh-Hans/client-blightmud.md @@ -0,0 +1,46 @@ +--- +kind: client +slug: blightmud +title: Blightmud +summary: 一个用 Rust 写的现代终端客户端,有 Lua 脚本、内置的文本转语音,以及一个会向服务器自报的屏幕阅读器模式。 +home: https://github.com/Blightmud/Blightmud +platform: Linux +platform: macOS +platform: Windows (WSL only) +capability: screen reader | yes | https://github.com/Blightmud/Blightmud +capability: TLS | yes | https://github.com/Blightmud/Blightmud +capability: UTF-8 | yes | https://github.com/Blightmud/Blightmud +capability: MCCP | yes | https://github.com/Blightmud/Blightmud +capability: GMCP | yes | https://github.com/Blightmud/Blightmud +capability: MSDP | yes | https://github.com/Blightmud/Blightmud +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/Blightmud/Blightmud +see-also: clients/tintin +see-also: clients/mudlet +see-also: protocols/ttype +--- + +Blightmud 是一个用 Rust 写的终端客户端,GPL 3,也是本节中发布最活跃的客户端之一。脚本是 Lua。它只 +在终端里跑:没有原生的 Windows 构建,Windows 用户是在 WSL 下运行它。 + +## 无障碍 + +Blightmud 在这方面有三块彼此不同的东西,比一行所能承载的要多: + +- 一个**对屏幕阅读器友好的模式**(`--reader-mode`,或 `reader_mode` 设置),它把终端界面改成阅读器 + 跟得上的样子。它不支持状态区。 +- **内置的文本转语音**,作为一个可选的编译项,并带有脚本可以调用的 Lua API——其中包括一个 + `tts.gag()`,用来让匹配到的行不被读出来。文档很坦白地说明,把它的 TTS 和屏幕阅读器一起用,未必总 + 是一个愉快的组合。 +- **自动的 MTTS 通告**:在阅读器模式下,或者启用了 TTS 时,它会把 `MTTS_SCREEN_READER` 加进它向服 + 务器自述的内容里,这样在意这一点的游戏就可以做出调整。 + +和 TinTin++ 一样,这里没有点名任何具体的屏幕阅读器,所以这是一个有文档的模式,而不是与某个产品经过 +测试的兼容性。 + +## 表里写着未知的地方 + +**MXP**、**MSP** 和 **ATCP** 在项目的 README 和随附的帮助里都不曾出现。**MCCP** 有文档说明是 v2; +v1 是否也一并处理,我们没有确立。 diff --git a/content/reference/zh-Hans/client-mudlet.md b/content/reference/zh-Hans/client-mudlet.md new file mode 100644 index 0000000..732e6f1 --- /dev/null +++ b/content/reference/zh-Hans/client-mudlet.md @@ -0,0 +1,50 @@ +--- +kind: client +slug: mudlet +title: Mudlet +summary: 跨平台,用 Lua 编写脚本,也是本节中屏幕阅读器支持的文档写得最详尽的客户端。 +home: https://www.mudlet.org/ +platform: Windows +platform: macOS +platform: Linux +capability: screen reader | yes | https://wiki.mudlet.org/w/Manual:Screen_Readers +capability: TLS | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: UTF-8 | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MCCP | unknown | +capability: GMCP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MSDP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: ATCP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MXP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: MSP | yes | https://wiki.mudlet.org/w/Manual:Supported_Protocols +capability: scripting | yes | https://github.com/Mudlet/Mudlet +see-also: clients/blightmud +see-also: clients/tintin +see-also: protocols/gmcp +see-also: connecting +--- + +Mudlet 是一个图形客户端,带地图器、一套包系统,以及一个 Lua API——它自己的大部分功能都是针对这个 +API 写出来的。它是 GPL,发布活跃,也是给一个刚要开始玩现代战斗 MUD 的人的常规推荐。 + +## 无障碍 + +这是本节中有文档支撑的理由最强的客户端,而这里的“有文档”是什么意思值得说清楚,因为它并不常见。 + +Mudlet 有**一章讲屏幕阅读器的手册内容**,有按操作系统分开的页面,点名了 Windows 上的 Narrator、 +NVDA 和 JAWS,Linux 上的 Orca,以及 macOS 上的 VoiceOver;有一条客户端内的 `mudlet access on` 命 +令,还有一个通过阅读器播报游戏来文的选项。它另有一个设置,会通过 MTTS 向服务器通告正在使用屏幕阅 +读器,这样游戏若愿意就可以做出调整。 + +它对自己哪里做得不好也很坦白:它自己的 Windows 页面说 JAWS 读输出窗口的方式和别的阅读器不一样,并 +建议改用 Narrator 或 NVDA。一个把自己无障碍支持行不通的情形也公布出来的项目,给你的信息比一个只公 +布一个对勾的项目要好。 + +## 表里写着未知的地方 + +**MCCP。**Mudlet 的源码实现了 MCCP v1 和 v2,但手册的支持协议页面没有把它列出来,而本节的规则是: +一项能力主张要引用项目自己的文档。从头文件里读出一个常量不是同一回事,所以这个格子写的是未知。 + +## 关于编码的说明 + +Mudlet 默认的服务器数据编码是 ASCII 而不是 UTF-8,CHARSET 协商是 4.10 才有的。如果在一个全新的配置 +档上某个游戏的文本显示不对,那个设置是第一个该看的地方。 diff --git a/content/reference/zh-Hans/client-mushclient.md b/content/reference/zh-Hans/client-mushclient.md new file mode 100644 index 0000000..a3ba360 --- /dev/null +++ b/content/reference/zh-Hans/client-mushclient.md @@ -0,0 +1,46 @@ +--- +kind: client +slug: mushclient +title: MUSHclient +summary: 老牌的 Windows 客户端。五种脚本语言,一套承载了它大部分协议支持的插件架构,以及一段已经放缓的发布史。 +home: https://www.mushclient.com/ +platform: Windows +platform: Linux (Wine) +capability: screen reader | unknown | +capability: TLS | unknown | +capability: UTF-8 | unknown | +capability: MCCP | yes | https://www.mushclient.com/mushclient/mccp.htm +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | yes | https://www.mushclient.com/gmcp +capability: MXP | yes | https://www.mushclient.com/mushclient/doc/general/features.html +capability: MSP | yes | https://github.com/nickgammon/mushclient/blob/master/plugins/msp.xml +capability: scripting | yes | https://www.mushclient.com/mushclient/doc/general/features.html +see-also: clients/mudlet +see-also: clients/potato +see-also: protocols/mccp +--- + +MUSHclient 是 Nick Gammon 的 Windows 客户端,MIT 许可,很长一段时间里是所有 Windows 用户的默认答 +案。它可以用 Lua、VBScript、JScript、PerlScript 和 Python 写脚本,而它所做的很多事是由插件、而不是 +由内核来承担的——这是一个货真价实的架构选择,也是上面好几行比看上去更难回答的原因。 + +最后一个打了标签的发布是 **5.06,2019 年 3 月**。仓库仍在被提交,也已经有了一个尚未发布的 5.07 的 +发布说明。 + +## 为什么这么多行写着未知 + +它们每一个都是诚实答案为“我们无法确立”的情形,而理由各不相同: + +- **GMCP**——项目自己关于它的页面给出的是一个你可以自己写的*示例*插件,而不是这个客户端具备的功 + 能。那和随产品提供支持是两回事,所以那个格子写的是未知,而不是有。 +- **TLS**——有文档的办法是外挂一个 `stunnel` 进程。一个加入 OpenSSL 支撑的 TLS 的提交在 2026 年落进 + 了 master 分支,但不在任何一个发布版里,所以今天用户装得到的东西里,没有我们指得出来的。 +- **UTF-8**——CHARSET 协商出现在尚未发布的 5.07 说明里,而在任何一个已发布版本的文档里,我们都没能 + 找到它。 +- **MSDP**——无论有无都没有说法。 +- **屏幕阅读器**——客户端随附一个用 Windows SAPI 的文本转语音插件,而那和屏幕阅读器支持不是一回 + 事。手册里没有无障碍章节,而作者本人在自己的论坛上说明过输出窗口为什么对阅读器不好用:它没有“当 + 前行”这个概念。我们无法确立一个答案,所以这张表也不给出一个。 + +这些没有一个是*否*。其中好几项很可能是有,只是我们没能证明。 diff --git a/content/reference/zh-Hans/client-potato.md b/content/reference/zh-Hans/client-potato.md new file mode 100644 index 0000000..16349c9 --- /dev/null +++ b/content/reference/zh-Hans/client-potato.md @@ -0,0 +1,42 @@ +--- +kind: client +slug: potato +title: Potato MUSHclient +summary: 一个为 MUSH 玩家写的跨平台 Tcl/Tk 客户端。编码支持不错,而它的整套文档对大多数协议只字未提。 +home: https://www.potatomushclient.com/ +platform: Windows +platform: Linux +platform: macOS +capability: screen reader | unknown | +capability: TLS | yes | https://github.com/potatomushclient/potato/wiki/ConfigureWorldsBasics +capability: UTF-8 | yes | https://github.com/potatomushclient/potato/wiki/Features +capability: MCCP | unknown | +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/potatomushclient/potato/wiki/FAQs +see-also: clients/beipmu +see-also: clients/mushclient +see-also: collaborative-roleplay +--- + +Potato 是一个为 MUSH 玩法打造的 Tcl/Tk 客户端——多个世界、spawn 窗口,以及一套假定你敲的是 pose(扮 +演描述)而不是战斗命令的默认设置。它用同一份源码在 Windows、Linux 和 macOS 上运行,其中 macOS 的构 +建通常落后一两个版本。 + +它会协商字符编码,并且完整支持 Unicode,对这个爱好中 MUSH 那一边来说,这是实践中最要紧的一项能力。 + +有一处有文档说明的限制值得注意:它支持连接到一个从一开始就是 SSL 的端口,而它自己的配置页面说, +STARTTLS 那种协商式 SSL **不**受支持。 + +## 为什么有六行写着未知 + +我们在项目的主页、下载页、全部 103 个维基帮助文件以及整棵源码树里都搜过 GMCP、MSDP、MCCP、MXP、 +MSP 和 ATCP。关于它们中的任何一个,都没有成文的说法。确实有*代码*碰到了其中一些,而本节不会把代码 +变成一项能力主张——一张仅凭头文件里的一个常量就写下“有”的表,是在做一个项目从未做过的承诺。 + +屏幕阅读器那一行是用同样的办法得到的同样的答案:对项目发布的一切做一次不区分大小写的搜查,找 +“screen reader”、“text-to-speech”、NVDA、JAWS、VoiceOver、“accessibility”、“visually impaired”和 +“blind”,结果什么也没有。这不是关于这个软件的发现。 diff --git a/content/reference/zh-Hans/client-tintin.md b/content/reference/zh-Hans/client-tintin.md new file mode 100644 index 0000000..fa2e0b2 --- /dev/null +++ b/content/reference/zh-Hans/client-tintin.md @@ -0,0 +1,47 @@ +--- +kind: client +slug: tintin +title: TinTin++ +summary: 一个有自己脚本语言的终端客户端,在每一个平台上都能跑,手机也不例外,并且有一个成文的屏幕阅读器模式。 +home: https://tintin.mudhalla.net/ +platform: Linux +platform: macOS +platform: Windows +platform: Android +platform: iOS +capability: screen reader | yes | https://tintin.mudhalla.net/manual/screen_reader.php +capability: TLS | yes | https://github.com/scandum/tintin +capability: UTF-8 | yes | https://github.com/scandum/tintin +capability: MCCP | yes | https://tintin.mudhalla.net/ +capability: GMCP | yes | https://tintin.mudhalla.net/manual/event.php +capability: MSDP | yes | https://tintin.mudhalla.net/manual/msdp.php +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://github.com/scandum/tintin +see-also: clients/blightmud +see-also: clients/mudlet +see-also: protocols/msdp +see-also: protocols/ttype +--- + +TinTin++ 是一个命令行客户端,GPL 3,发布活跃,而且它能跑的地方比这里的任何其他客户端都多——包括 +Android 和 iOS。它的脚本语言是它自有的,很简练,能做的事情却很多;别的客户端在图形界面里做的相当 +一部分事情,在这里是一行 `#config`。 + +**MSSP** 和 **MSDP** 的协议规范由同一位作者维护,这也是本节里那么多协议页面都引用同一个站点的原因。 + +## 无障碍 + +TinTin++ 有一个专门讲**屏幕阅读器模式**的手册页面(`#config screen reader on`,或者启动时加 +`-s`)。启用它会做两件事:把念出来没有意义的视觉元素去掉或改掉,以及通过 +[MTTS](/reference/protocols/ttype) 向服务器报告正在使用屏幕阅读器,这样游戏就可以调整自己的输出。 + +那是一个有文档的模式,不是与某个具体阅读器测试过的主张——那一页上没有点名任何产品。作为证据,它明 +显弱于一个点名了自己配合哪些阅读器的客户端,也明显强于什么都没有。 + +## 表里写着未知的地方 + +**MXP** 和 **MSP** 在项目站点上都有社区脚本,而一个脚本不等于客户端支持某个协议——MXP 那个脚本直接 +说了它未必能在每个 MUD 上工作。两者的原生支持都未能确立。**ATCP** 我们无论有无都没找到任何说法;顺 +带一提,ATCP 大体上已被 GMCP 取代,而 GMCP 是 TinTin++ 支持的。 diff --git a/content/reference/zh-Hans/client-tinyfugue.md b/content/reference/zh-Hans/client-tinyfugue.md new file mode 100644 index 0000000..e27d31e --- /dev/null +++ b/content/reference/zh-Hans/client-tinyfugue.md @@ -0,0 +1,40 @@ +--- +kind: client +slug: tinyfugue +title: TinyFugue +summary: 经典的 UNIX 终端客户端。上游自 2007 年起就没有再发布过;一个仍在维护的分支把它带了下去。 +home: https://tinyfugue.sourceforge.net/ +platform: Linux +platform: macOS +platform: BSD +capability: screen reader | unknown | +capability: TLS | yes | https://tinyfugue.sourceforge.net/ +capability: UTF-8 | unknown | +capability: MCCP | yes | https://tinyfugue.sourceforge.net/ +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | unknown | +capability: scripting | yes | https://tinyfugue.sourceforge.net/ +see-also: clients/tintin +see-also: clients/blightmud +--- + +TinyFugue——“tf”——是 MUSH 世界很大一部分人用了二十年的终端客户端,输入和输出分成两个窗格,有一门自 +己的宏语言,还有一套比它好几个竞争者活得更久的使用习惯。 + +**上游处于休眠状态**:最后一个发布是 5.0 beta 8,2007 年 1 月。它仍然能编译,也仍然能用。 + +一个仍在维护的分支 *TinyFugue Rebirth* 发布活跃,在原生宏语言之外加入了 GMCP、ATCP、经由 ICU 的宽 +字符支持,以及 Python 和 Lua 脚本。上面那张表描述的是**上游**,因为“TinyFugue”指向的就是上游;如果 +你今天要装,那个分支值得先看一眼。 + +## 这个客户端的文档里的陷阱 + +上游有一个叫**“non-visual mode”**的文档条目。它跟辅助技术无关——它讲的是把输入限制在最底下那一 +行——而且从头到尾没有提到屏幕阅读器、语音,也没有提到盲人用户。一张靠关键词搜索拼出来的能力表,会 +把那个文件名变成一个“有”。这一张写的是未知,因为文档所能支撑的只有这个。 + +UTF-8 是同样形状的答案:有文档的编码支持是针对 8 位的 ISO 8859 字符集,而关于 UTF-8,我们无论有无 +都没有找到上游的任何说法。 diff --git a/content/reference/zh-Hans/client-vipmud.md b/content/reference/zh-Hans/client-vipmud.md new file mode 100644 index 0000000..4a29ea7 --- /dev/null +++ b/content/reference/zh-Hans/client-vipmud.md @@ -0,0 +1,38 @@ +--- +kind: client +slug: vipmud +title: VIP Mud +summary: 一个从头为盲人玩家打造的商业 Windows 客户端。它点名了七款屏幕阅读器——却几乎没有公布任何关于协议支持的内容。 +home: https://www.gmagames.com/vipmud.shtml +platform: Windows +capability: screen reader | yes | https://www.gmagames.com/vipmud.shtml +capability: TLS | unknown | +capability: UTF-8 | unknown | +capability: MCCP | unknown | +capability: GMCP | unknown | +capability: MSDP | unknown | +capability: ATCP | unknown | +capability: MXP | unknown | +capability: MSP | yes | https://www.gmagames.com/vipmud.shtml +capability: scripting | yes | https://www.gmagames.com/vipmud.shtml +see-also: clients/mudlet +see-also: clients/blightmud +--- + +VIP Mud 是本节中*整个*设计前提就是无障碍的那一个客户端。它是商业软件——三十美元,有三十天的完整试 +用,试用期过后它仍然能用,只是功能有所削减——而且它是一个 Windows 程序。 + +它是这里遥遥领先的最强无障碍主张,而且难得地具体。产品页面点名 **JAWS、Window-Eyes、System +Access、NVDA、Cobra、SuperNova/Hal 和 Microsoft SAPI** 开箱即用,并描述了一些只有认真想过这个问题 +才会做出来的功能:按窗口、按输出类型使用不同的语音;把刷屏内容从语音里屏蔽掉、却依然显示出来;以 +及好几种抑制 ASCII 图画的办法——那是一个 MUD 发给屏幕阅读器的东西里最不友善的一样。 + +## 为什么这张表的其余部分是空的 + +因为厂商发布的是一个营销页面,而不是一份手册。上面没有任何地方提到 GMCP、MSDP、MCCP、MXP、ATCP、 +TLS 或字符编码;它把产品描述为“a Telnet-based client”,然后就没有下文了。**连着九个未知不是对这个 +软件的判决。**当唯一可得的来源只有一页时,一张矩阵就长这个样子,而把它发布成九个“否”,会是对一个 +很可能全都做得到的产品撒谎。 + +还有两件我们无法确立的事:当前版本的任何发布日期,以及它是否仍在积极开发中——这家厂商于 2025 年 2 +月被收购,而产品页面上标的版权年份是 2016。 diff --git a/content/reference/zh-Hans/codebase-aresmush.md b/content/reference/zh-Hans/codebase-aresmush.md new file mode 100644 index 0000000..fc8a951 --- /dev/null +++ b/content/reference/zh-Hans/codebase-aresmush.md @@ -0,0 +1,29 @@ +--- +kind: codebase +slug: aresmush +title: AresMUSH +summary: 一个用 Ruby 写成的现代扮演服务器,自带 Web 前端和场景工具,而不是靠 softcode 写出来的。 +codebase: AresMUSH +home: https://aresmush.com/ +see-also: collaborative-roleplay +see-also: codebases/pennmush +see-also: codebases/evennia +--- + +AresMUSH 是目前广泛使用的服务器中最新的一个,明确瞄准**协作扮演**,它所取的立场与它接续的 +TinyMUSH 一脉不同。PennMUSH 游戏的场景系统、角色卡和工单队列,是当时在场的人用 softcode 一点点搭 +出来的;Ares 则把这些直接作为功能提供,并且期待游戏的管理人员去配置它们,而不是去编写它们。 + +它自带一个 **Web 门户**——角色维基、场景日志、论坛以及游戏本身,全都可以从浏览器访问——对于一个人 +们事后要读日志的题材来说,这是种类上的差别,而不只是程度上的差别。 + +配置写在 YAML 里;扩展是 Ruby 插件。玩家没有游戏内的编程语言可用,这就是它的取舍:绳子少了,被绳 +子勒伤的机会也少了,而 MUSH 一脉赖以得名的那种即兴营造文化,同样少了。 + +## 从外面看是什么样 + +没有 MSSP。它会回应登录前的 `WHO`,而回应是一份**逐个玩家的列表**,不是一个光秃秃的数字,我们的解 +析器按结构来数它。在我们实测的那个游戏上,没有协商任何 telnet 选项。 + +如果你要为一个新的扮演游戏在它和 PennMUSH 之间做选择,问题大致是:你想要一个由你配置的系统,还是 +一个由你编写的系统。 diff --git a/content/reference/zh-Hans/codebase-circlemud.md b/content/reference/zh-Hans/codebase-circlemud.md new file mode 100644 index 0000000..dfea900 --- /dev/null +++ b/content/reference/zh-Hans/codebase-circlemud.md @@ -0,0 +1,26 @@ +--- +kind: codebase +slug: circlemud +title: CircleMUD +summary: 文档写得完善到足以拿来教学的 DikuMUD 衍生代码库。被派生的次数多得惊人;至今仍在运行。 +codebase: CircleMUD +home: https://www.circlemud.org/ +see-also: codebases/dikumud +see-also: codebases/tbamud +see-also: codebases/rom +--- + +CircleMUD 是 DikuMUD 的一个衍生版本,而它的与众不同之处从来不是某个游戏机制,而是**文档**。Jeremy +Elson 发布的代码干净、有注释,还附带一份编码指南,结果它成了人们用来学 C、用来跑一个 MUD、并且不 +必先做逆向工程就能派生的那个代码库。 + +由此带来的结果是,大量仍在运行的游戏都是隔了好几代的 Circle 衍生物,而这个词往往不会出现在任何玩 +家看得到的地方。 + +Circle 本身的开发早已结束;**tbaMUD** 是它的延续,今天还在维护的 Circle 游戏,通常是作为 tbaMUD 在 +维护。 + +## 从外面看是什么样 + +有 MSSP,索取即答。登录画面上没有 `WHO`——Diku 家族一般不提供,所以一个只读登录画面的目录,在这里 +什么也看不到。 diff --git a/content/reference/zh-Hans/codebase-cobramush.md b/content/reference/zh-Hans/codebase-cobramush.md new file mode 100644 index 0000000..604e4b4 --- /dev/null +++ b/content/reference/zh-Hans/codebase-cobramush.md @@ -0,0 +1,22 @@ +--- +kind: codebase +slug: cobramush +title: CobraMUSH +summary: 一个 PennMUSH 分支,有自己的分部(division)与权限模型。部署量不大,仍在应答。 +codebase: CobraMUSH +home: https://cobramush.org/ +see-also: codebases/pennmush +see-also: codebases/rhostmush +--- + +CobraMUSH 从 PennMUSH 分支而来,加入了一套*分部*(division)模型——一个可以逐级下放权限的管理权层 +级,取代了它上游所用的 wizard/royalty 那种扁平区分。想把管理权切成小块分出去、又不愿把全部权限一 +并交出的游戏,就是它的用户群。 + +为 PennMUSH 写的 softcode 大体上能跑,差异恰好集中在这次分支所针对的那个领域。 + +## 从外面看是什么样 + +没有 MSSP,登录前的 `WHO` 可用,而在我们实测的那个游戏上完全没有协商任何 telnet 选项。最后这一点不 +是批评:什么都不协商的服务器,也就不可能把协商弄错,而纯文本走一个普通套接字,是这个爱好里每一个 +客户端都应付得来的东西。 diff --git a/content/reference/zh-Hans/codebase-coffeemud.md b/content/reference/zh-Hans/codebase-coffeemud.md new file mode 100644 index 0000000..07fe509 --- /dev/null +++ b/content/reference/zh-Hans/codebase-coffeemud.md @@ -0,0 +1,25 @@ +--- +kind: codebase +slug: coffeemud +title: CoffeeMUD +summary: 一个用 Java 写的 MUD 服务器,拥有我们探测过的一切当中最大的一份 MSSP 报告,协议面也宽得反常。 +codebase: CoffeeMUD +home: https://www.coffeemud.net/ +see-also: codebases/dikumud +see-also: protocols/mssp +--- + +CoffeeMUD 是一个 Java 编写的 MUD 服务器,功能面宽得反常——它自带 Web 服务器、邮件、论坛和一套庞大 +的职业与技能系统,而且它是这个爱好里少数几个不是用 C 写的服务器之一。 + +它在积极维护中,按本目录这一部分的标准,这是值得大声说出来的一件事。 + +## 从外面看是什么样 + +有 MSSP 和 **MCCP2**,而且在我们试过的二十台服务器里,CoffeeMUD 是仅有的三台会同时回应*明文* +`MSSP-REQUEST` 形式的服务器之一——这种形式比那个 telnet 选项还早,如今偶尔还能见到。 + +它的 MSSP 报告是我们实测到的最大的一份:**47 个字段**,其中 `PORT` 为九个不同的端口分九次报出。这 +不是格式错误。MSSP 变量本来就是列表,而一个把多值 `PORT` 压平成单个字符串的爬虫,会从 +`"80" "23" "4201"` 造出整数 `80234201`——这个 bug 本项目发布过,也修复了,也正是这里的解析器自始至 +终把取值保留为列表的原因。 diff --git a/content/reference/zh-Hans/codebase-dikumud.md b/content/reference/zh-Hans/codebase-dikumud.md new file mode 100644 index 0000000..687c517 --- /dev/null +++ b/content/reference/zh-Hans/codebase-dikumud.md @@ -0,0 +1,31 @@ +--- +kind: codebase +slug: dikumud +title: DikuMUD +summary: 战斗类 MUD 家族的根。等级、职业、装备和区域文件——以及一份塑造了整整一代衍生代码库的许可协议。 +codebase: DikuMUD +home: https://dikumud.com/ +see-also: codebases/circlemud +see-also: codebases/rom +see-also: codebases/smaug +see-also: mush-mud-muck-moo +--- + +DikuMUD 写于哥本哈根大学的 Datalogisk Institut,1991 年发布,人们不加限定地说“MUD”时所指的东西, +大多以它为祖先。等级、角色职业、生命值、mob、装备栏位、一种由建造者离线编写的区域文件格式——整套 +词汇都出自这里,而那些从未见过 Diku 源码的游戏,依然继承了它的形状。 + +它的许可协议也是故事的一部分。Diku 可以免费使用,但禁止对访问收费,并要求显示原始致谢,正是这一条 +款,使得“Diku 致谢”会出现在与它隔了好几次分支的游戏的登录画面上。 + +它的直系后代——**Merc**,然后是 **ROM**、**CircleMUD**、**SMAUG**、**tbaMUD** 以及其他几十个——在有 +史以来的每一份 MUD 名录里都占了很大一部分。 + +## 从外面看是什么样 + +Diku 家族就是 **MSSP** 家族。MUSH 那一边通过登录画面上的 `WHO` 发布人数,完全不提供 MSSP;而 Diku +一脉的服务器绝大多数会用一份结构化报告回应 telnet 选项 70,本站上它们的数字正是从那里来的。 + +**MCCP2**——流压缩——在这个家族里也很常见,而且值得知道的是:一个协商了它、却无法解压这条流的客户 +端,收到的整个连接画面会是一堆二进制噪声。这曾是本项目所依赖的 telnet 库里一个货真价实的缺陷,现 +已修复;见 [MCCP](/reference/protocols/mccp)。 diff --git a/content/reference/zh-Hans/codebase-evennia.md b/content/reference/zh-Hans/codebase-evennia.md new file mode 100644 index 0000000..7ebfdaf --- /dev/null +++ b/content/reference/zh-Hans/codebase-evennia.md @@ -0,0 +1,30 @@ +--- +kind: codebase +slug: evennia +title: Evennia +summary: 一个 Python 框架,而不是一个做好的游戏。两个 Evennia 游戏之间,可能除了管道什么都不共享。 +codebase: Evennia +home: https://www.evennia.com/ +see-also: codebases/aresmush +see-also: collaborative-roleplay +see-also: protocols/gmcp +--- + +Evennia 是一个 **MU\* 框架**,不是一个游戏——这是关于它首先要知道的事,也正是这一点让互相比较各个 +Evennia 游戏变得没什么意义。它是一个建立在 Django 和 Twisted 之上的 Python 库,为你提供账户、对 +象、房间、命令、一层持久化和整套网络栈,然后指望你把游戏写出来。 + +由此带来的结果是,“运行 Evennia”所告诉你的东西,远少于“运行 PennMUSH”。Evennia 上有战斗 MUD,也有 +扮演游戏,它们不共享任何词汇。两个 Evennia 游戏可能连一条相同的命令都没有。 + +对一个已经会 Python 的开发者来说,这是从零到一个能跑的世界的最短路径,二〇一〇年代中期以来相当一 +部分新游戏也正是从这里起步的。 + +## 从外面看是什么样 + +Evennia 提供 **MSSP**,并通过它发布玩家人数。在我们实测的那个游戏上,它还协商了 **MCCP2**——压 +缩——这是一个认真对待自己 telnet 实现的技术栈的特征。 + +因为 Evennia 是一个框架,某个具体游戏协商什么,有一部分是那个游戏自己的决定。协议页面上的采用数字 +数的是服务器实际提供给我们的东西,而不是这个框架能做到的东西,对 Evennia 来说,这两者之间的距离比 +大多数情况都要远。 diff --git a/content/reference/zh-Hans/codebase-fluffos.md b/content/reference/zh-Hans/codebase-fluffos.md new file mode 100644 index 0000000..2729d7b --- /dev/null +++ b/content/reference/zh-Hans/codebase-fluffos.md @@ -0,0 +1,29 @@ +--- +kind: codebase +slug: fluffos +title: FluffOS +summary: 仍在维护的 MudOS 后继者,也是幸存的 LPMud 游戏大多所用的驱动。游戏是用 LPC 写的,不是用 C。 +codebase: FluffOS +home: https://www.fluffos.info/ +see-also: codebases/dikumud +see-also: mush-mud-muck-moo +--- + +LPMud 传统对世界的切分方式与 Diku 不同。这里有一个**驱动**——一个运行面向对象解释器的 C 程序——还 +有一个 **mudlib**,那才是整个游戏,用 **LPC** 写成,由驱动加载。房间、战斗、命令和登录流程全都是 +mudlib 对象;驱动对它们一无所知。 + +这使得一个 LPMud 在气质上比它的战斗系统所暗示的更接近 MUSH:游戏是用一种活在游戏内部的语言写的, +而两个共用同一个驱动的 LPMud,可能别的什么都不共享。 + +**MudOS** 多年来一直是主流驱动;**FluffOS** 是它仍在维护的延续,也是今天一个还在运行的 LP 游戏最可 +能跑在上面的东西。有名的 mudlib——Nightmare、Lima、Discworld 自己的那套——则又是各自独立的项目。 + +## 从外面看是什么样 + +在我们实测的那个 FluffOS 游戏上有 MSSP 和 **MCCP2**。在我们的调查中,MudOS 是仅有的两个既回应 +MSSP、又回应登录画面 `WHO` 的代码库之一,不过它给出的 `WHO` 是一份逐个玩家的清单,而不是一个计数。 + +因为 mudlib 就是游戏,任何一个具体的 LP 游戏协商什么,是 mudlib 的决定不亚于是驱动的决定——协议页 +面上的采用数字数的是服务器实际提供给我们的东西,对这个家族而言,那作为关于代码库的信号,要比在别 +处更弱。 diff --git a/content/reference/zh-Hans/codebase-moo.md b/content/reference/zh-Hans/codebase-moo.md new file mode 100644 index 0000000..8b1bd8c --- /dev/null +++ b/content/reference/zh-Hans/codebase-moo.md @@ -0,0 +1,26 @@ +--- +kind: codebase +slug: moo +title: MOO +summary: 面向对象,完全从内部编辑,与其说是游戏引擎,不如说同样是一个研究与教学平台。 +codebase: MOO +home: https://www.ipomoea.org/moo/ +see-also: mush-mud-muck-moo +see-also: codebases/muck +--- + +MOO——*MUD, Object-Oriented*——把“世界自己编辑自己”这个想法推得比这个爱好里的任何东西都远。最初的 +服务器 LambdaMOO 只带一个很小的 C 内核和一个数据库;用户体验到的几乎一切,都是**用 MOO 语言、在运 +行中的数据库里、由使用它的人写出来的**。一个房间没有对应的源文件。 + +这个性质让 MOO 拥有了游戏之外的生命。整个九十年代里,它们被用于教学、会议和研究——Diversity +University、BioMOO、Jay's House——而关于 MOO 的技术文献,对这个领域的一个代码库来说,学术味重得不 +成比例。 + +今天的部署量很小,但确确实实不是零,而留下来的那些服务器往往已经连续运行了几十年。 + +## 从外面看是什么样 + +没有 MSSP,在我们实测的那个游戏上也没有我们能解析的 `WHO`。它有的是连接画面里的一句话,写着*“one +of three players are active”*——这个爬虫里读拼写出来的数词的那一块,正是由此而来。一个只认数字的解 +析器在那里根本看不到任何计数,并且会把那个游戏永远报成未知。 diff --git a/content/reference/zh-Hans/codebase-muck.md b/content/reference/zh-Hans/codebase-muck.md new file mode 100644 index 0000000..04cbf40 --- /dev/null +++ b/content/reference/zh-Hans/codebase-muck.md @@ -0,0 +1,27 @@ +--- +kind: codebase +slug: muck +title: MUCK +summary: TinyMUD 的一支后裔,有自己的类 Forth 游戏内语言,社群文化也与 MUSH 那一边不同。 +codebase: MUCK +home: https://www.fuzzball.org/ +see-also: mush-mud-muck-moo +see-also: codebases/tinymush +see-also: codebases/moo +--- + +MUCK——实际上几乎总是指 **Fuzzball MUCK**——是 MUSH 一脉的兄弟,而不是它的后代:两者都源自 TinyMUD, +也都在游戏里放了一门编程语言。 + +语言就是那处看得见的差别。MUF(*Multi-User Forth*)是基于栈的,读起来和 MUSH softcode 毫无相似之 +处;精通其中一门的建造者,在另一门面前是新手。在它之上还有 MPI,一门更小的内联表达式语言,用来做 +在 MUSH 上会交给 softcode 去做的那些事。 + +从文化上说,MUCK 是这个爱好中社交与同人世界很大一部分的家园。那些游戏往往围绕在场与交谈来构建,而 +不是围绕有开始有结束的场景,这是与扮演 MUSH 传统之间一处实实在在的差别,而不是题材问题。 + +## 从外面看是什么样 + +没有 MSSP。登录前的 `WHO` 会给出一个计数。在我们实测的那个游戏上没有协商任何 telnet 选项——调查中 +还有一个细节值得记住:它的 `WHO` 回应以一个尾随空格结束,没有换行,而这正是那种会让一个天真的解析 +器什么也报不出来的东西。 diff --git a/content/reference/zh-Hans/codebase-pennmush.md b/content/reference/zh-Hans/codebase-pennmush.md new file mode 100644 index 0000000..dc3ccd2 --- /dev/null +++ b/content/reference/zh-Hans/codebase-pennmush.md @@ -0,0 +1,44 @@ +--- +kind: codebase +slug: pennmush +title: PennMUSH +summary: 部署最广的 MUSH 服务器。softcode、一段很长的发布史,以及在我们的调查中仅有的两个既回应 MSSP 又回应登录前 WHO 的代码库之一。 +codebase: PennMUSH +home: https://www.pennmush.org/ +see-also: codebases/tinymux +see-also: codebases/rhostmush +see-also: codebases/cobramush +see-also: mush-mud-muck-moo +see-also: protocols/mssp +--- + +PennMUSH 经由 1991 年的一次分支从 TinyMUSH 而来,也是长期运行的扮演 MUSH 最常用的服务器。它的决定 +性特征是 **softcode**:一门函数式表达式语言,由任何设置了相应标记位的人从游戏内部编辑,任何一个 +MUSH 的行为都有很大一部分是用它写的。与其说一个 PennMUSH 游戏是被配置出来的,不如说是被它的玩家编 +程出来的。 + +版本号形如 `1.8.8p0`——一个主版本、一个次版本和一个补丁级别——而补丁级别经常变动。游戏常常跑在落后 +好几个补丁级别的版本上,这并不稀奇。 + +## 从外面看是什么样 + +在我们自己那次涵盖 38 台服务器的调查中,PennMUSH 是仅有的两个把我们探测的*两条*路径都回应了的代码 +库之一。它在被问及时提供 MSSP,也会回应在登录画面上敲入的 `WHO`,而在我们实测的那个游戏上,两者是 +一致的——这比听上去要罕见,也让 PennMUSH 成了我们用来对照其他服务器的基准。 + +登录前的 `WHO` 的意义不止于方便:MUSH 家族能发布玩家人数,靠的就是它,因为这个家族的其余大部分根 +本不提供 MSSP。这一分裂正是本站要探测四层而不是一层的原因,见 +[MSSP](/reference/protocols/mssp)。 + +在现代 PennMUSH 上,CHARSET 协商是常态,这也是带重音符号的名字能平安走完全程的原因。 + +## 相关服务器 + +PennMUSH、**TinyMUX**、**RhostMUSH** 和 **CobraMUSH** 是四个有共同祖先、共享一套词汇的服务器——懂其 +中一个的建造者,费点力气也能读懂另一个的 softcode。它们并不兼容:数据库不做一次转换是搬不过去的, +函数库的差异也大到会带来实际影响。 + +## SharpMUSH + +有一个以 PennMUSH 兼容为目标的 .NET 重新实现正在开发中,作者与本站相同。本页没有任何内容是从它实测 +出来的,目录里也没有它的游戏。 diff --git a/content/reference/zh-Hans/codebase-rhostmush.md b/content/reference/zh-Hans/codebase-rhostmush.md new file mode 100644 index 0000000..225d2d8 --- /dev/null +++ b/content/reference/zh-Hans/codebase-rhostmush.md @@ -0,0 +1,25 @@ +--- +kind: codebase +slug: rhostmush +title: RhostMUSH +summary: 一个以精细的权限模型和庞大的内置函数集著称的 MUSH 服务器。没有 MSSP;会回应登录前的 WHO。 +codebase: RhostMUSH +home: https://github.com/RhostMUSH/trunk +see-also: codebases/pennmush +see-also: codebases/tinymux +see-also: codebases/cobramush +--- + +RhostMUSH 是四个广泛使用的 TinyMUSH 系服务器中的第四个,也是管理模型最为繁复的一个:它的权限与标记 +系统比它的亲戚们细致得多,游戏选择它通常就是为了这个。 + +它的内置函数库很大,为 Rhost 写的 softcode 往往无法干净地移植到 PennMUSH 或 TinyMUX,除非把用到了 +其他服务器所没有的函数的那些部分重写掉。 + +## 从外面看是什么样 + +没有 MSSP。登录前的 `WHO` 会给出一个计数。CHARSET 有协商。 + +这个组合——没有 MSSP,`WHO` 可用——正是 MUSH 家族的标志,也正是本站要去探测登录画面的原因。以我们自 +己那次调查的证据来看,MSSP 家族和 `WHO` 家族几乎互不相交:28 个代码库通过 MSSP 发布人数,7 个通过 +`WHO`,两者都发布的只有 2 个。 diff --git a/content/reference/zh-Hans/codebase-rom.md b/content/reference/zh-Hans/codebase-rom.md new file mode 100644 index 0000000..e78898a --- /dev/null +++ b/content/reference/zh-Hans/codebase-rom.md @@ -0,0 +1,27 @@ +--- +kind: codebase +slug: rom +title: ROM +summary: Merc 最有名的后代,也是九十年代很大一部分 MUD 所建立在其上的战斗引擎。 +codebase: ROM +see-also: codebases/dikumud +see-also: codebases/smaug +see-also: protocols/mccp +--- + +ROM——*Rivers of MUD*——是 **Merc** 的一个衍生版本,而 Merc 本身又是 DikuMUD 的衍生版本,ROM 则是站 +住了脚的那一个。它的战斗模型、它的技能与法术系统以及它的区域格式,是九十年代及其后极大量游戏的起 +点,尤其是 ROM 2.4,是这个爱好里被派生得最多的源码之一。 + +和 Diku 一脉的其余部分一样,它带着原始致谢的要求,所以一个你无法从别处确定其血统的游戏,往往会在 +它的登录画面上同时提到 Diku、Merc 和 ROM。 + +## 从外面看是什么样 + +在我们实测的那个游戏上有 MSSP、CHARSET 和 **MCCP2**。 + +ROM 是本项目用来坐实自己那个压缩 bug 的服务器。我们的探测协商了 MCCP2,服务器正确地开始压缩,而我 +们所依赖的 telnet 库始终没有解压这条流——于是连接画面到达时是一整片替换字符,我们一度把它记成了那 +个游戏的问题。用一个现成的 zlib 调用,那段负载能干净地解压出来,正是这一点让事情不再含糊。它已在 +上游修复;故事写在 [MCCP](/reference/protocols/mccp) 页上,因为它是一个从外面看起来和坏掉的游戏一 +模一样的缺陷的好例子。 diff --git a/content/reference/zh-Hans/codebase-smaug.md b/content/reference/zh-Hans/codebase-smaug.md new file mode 100644 index 0000000..b2462a6 --- /dev/null +++ b/content/reference/zh-Hans/codebase-smaug.md @@ -0,0 +1,24 @@ +--- +kind: codebase +slug: smaug +title: SMAUG +summary: 一个为大型主题世界打造的 Merc 衍生版本,有神祇、位面,以及规模数倍于其上游的区域集。 +codebase: SMAUG +home: https://www.smaug.org/ +see-also: codebases/dikumud +see-also: codebases/rom +--- + +SMAUG——*Simulated Medieval Adventure multi-User Game*——出自 Realms of Despair,是一个只做加法、不 +做简化的 Merc 衍生版本。ROM 把 Merc 的战斗收紧了,SMAUG 则往里加:神祇、位面、更繁复的区域程序和 +mob 程序、大得多的标准世界,以及相应地大得多的源码树。 + +运行它的游戏往往是那些大型主题游戏,其中有几个已经连续运行了二十五年。 + +## 从外面看是什么样 + +有 MSSP,索取即答,在我们实测的那个游戏上没有协商别的任何东西。 + +给要比较人数的人提个醒:我们调查中的 SMAUG 服务器是通过 MSSP 报数的,并不回应登录画面上的 `WHO`, +所以这里的数字是游戏自己的 MSSP `PLAYERS`——那是服务器做出的一个主张,我们采信它,仅仅是因为我们无 +法独立地实测它。游戏页面会标明任何一个数字究竟来自两者中的哪一个。 diff --git a/content/reference/zh-Hans/codebase-tbamud.md b/content/reference/zh-Hans/codebase-tbamud.md new file mode 100644 index 0000000..8c58ab6 --- /dev/null +++ b/content/reference/zh-Hans/codebase-tbamud.md @@ -0,0 +1,23 @@ +--- +kind: codebase +slug: tbamud +title: tbaMUD +summary: CircleMUD 仍在维护的延续,也是“我今天想开一个 Diku 风格的 MUD”这个问题的常规答案。 +codebase: tbaMUD +home: https://tbamud.com/ +see-also: codebases/circlemud +see-also: codebases/dikumud +--- + +tbaMUD——*The Builder Academy MUD*——在 CircleMUD 自身开发停下的地方把它接了过来,此后一直在建设、 +打补丁和写文档。如果有人要从一个已知可靠的基底、而不是从自己的一个分支出发去做一个练级加战斗的 +MUD,这就是那个基底。 + +它把 Circle 代码库的优点延续了下来:可读的 C、一份有文档的区域文件格式,以及一本假定你从没做过这 +件事的建造者手册。The Builder Academy 本身就作为一个教学游戏在运行,人们在上面学怎么建造。 + +## 从外面看是什么样 + +有 MSSP,索取即答。在我们探测的那个游戏上是一个实测到的零——在本站,这是一个填满的格子,而不是一个 +空格子:我们连上了,我们读到了数字,而那个数字是零。这是与一个我们无法计数的游戏不同的事实,两者 +在这里从不合并。 diff --git a/content/reference/zh-Hans/codebase-tinymush.md b/content/reference/zh-Hans/codebase-tinymush.md new file mode 100644 index 0000000..dbf10db --- /dev/null +++ b/content/reference/zh-Hans/codebase-tinymush.md @@ -0,0 +1,37 @@ +--- +kind: codebase +slug: tinymush +title: TinyMUSH +summary: MUSH 一脉的祖先,至今仍有游戏在跑。它让这个爬虫明白了:自己发出的协商字节,会毁掉它接下来发送的那条命令。 +codebase: TinyMUSH +home: https://github.com/TinyMUSH/TinyMUSH +see-also: codebases/pennmush +see-also: codebases/tinymux +see-also: mush-mud-muck-moo +--- + +PennMUSH、TinyMUX、RhostMUSH 和 CobraMUSH 这一脉全都出自 TinyMUSH,而它至今仍有部署。它的开发是安 +静,而不是不存在。 + +## 从外面看是什么样 + +没有 MSSP。登录前的 `WHO` 会用一句这种形式的话作答: +`0 Players logged in, 22 record, no maximum.` + +## 它在我们身上找出的那个 bug + +TinyMUSH 值得在这里占一段,因为正是这个游戏暴露了本站自己爬虫里的一个缺陷,而这次订正很好地说明了 +“实测”该是什么意思。 + +我们的探测有好几周把 TinyMUSH 读成了*计数未知*。当时归档的猜测是它的回应没有尾随换行。它有。从线 +路上抓下来看,真正的原因在我们这边:**TinyMUSH 在它的登录画面上不解析 telnet**,所以我们连接时发出 +的 `IAC DO MSSP` 那三个字节,落进它的输入缓冲区,就像有人把它们敲了进去一样。它读到的下一行不是 +`WHO`,而是三个控制字节后面跟着 `WHO`,那不是它认识的命令——于是它重新显示自己的连接画面,对玩家人 +数只字不提。 + +现在探测会在协商之后发一个光秃秃的换行,并把它引出的任何东西丢弃,因为那份输出是对*我们*选择发送 +的字节的反应,因此既不是游戏的连接画面,也不是它的回答。TinyMUSH 现在读得正确了,探测耗时也只有原 +来的三分之一。 + +一个没有去查的目录,会在这个游戏存在的全部时间里发布“此游戏不报告其玩家人数”,而那句话说的其实是 +我们。 diff --git a/content/reference/zh-Hans/codebase-tinymux.md b/content/reference/zh-Hans/codebase-tinymux.md new file mode 100644 index 0000000..a6945ef --- /dev/null +++ b/content/reference/zh-Hans/codebase-tinymux.md @@ -0,0 +1,33 @@ +--- +kind: codebase +slug: tinymux +title: TinyMUX +summary: 另一个大牌 MUSH 服务器。softcode 与 PennMUSH 的接近到足以让人争论不休,完全没有 MSSP,而登录前的 WHO 是好用的。 +codebase: TinyMUX +home: https://www.tinymux.org/ +see-also: codebases/pennmush +see-also: codebases/tinymush +see-also: codebases/rhostmush +see-also: mush-mud-muck-moo +--- + +TinyMUX 是老牌扮演 MUSH 最常用的两个服务器中的第二个,而对许多玩家来说,在它和 PennMUSH 之间怎么选 +只取决于自家游戏的管理人员先学会了哪一个。版本号形如 `2.12` 之类。 + +和 PennMUSH 一样,它也出自 TinyMUSH,它的 softcode 接近到让一个在两者之间迁移的建造者是在做翻译, +而不是重新学。差异是实实在在的——函数库、若干解析上的边角、`@` 命令集——而且恰恰是那种会让在两者之 +间搬数据库变成一个项目、而不是一次导出的东西。 + +## 从外面看是什么样 + +**没有 MSSP。**TinyMUX 根本不提供这个选项,这把它和 AresMUSH、MUCK、RhostMUSH、CobraMUSH 以及 +TinyMUSH 一起归到了这个爱好中一个只认 MSSP 的目录压根看不见的那一边。它的玩家人数来自登录画面上的 +`WHO`,它会用一个朴素的计数作答。 + +它确实会协商 CHARSET,这也是它在非 ASCII 文本上胜过大多数亲戚的原因。 + +## 这些人数从哪里来 + +如果你要拿本站给某个 TinyMUX 游戏的数字去和另一个目录的比,请注意我们读的是登录画面上的 `WHO`,而 +大多数爬虫不读。一个只建立在 MSSP 之上的目录,会把这些游戏报成完全没有人数,或者干脆不把它们列出 +来。 diff --git a/content/reference/zh-Hans/orientation-collaborative-roleplay.md b/content/reference/zh-Hans/orientation-collaborative-roleplay.md new file mode 100644 index 0000000..bfad61e --- /dev/null +++ b/content/reference/zh-Hans/orientation-collaborative-roleplay.md @@ -0,0 +1,56 @@ +--- +kind: orientation +slug: collaborative-roleplay +title: 你想找的是协作扮演 +summary: 如果你想要的是和别人一起写场景,这里是一条捷径——该找哪些代码库、哪种客户端不会跟你作对,以及第一晚会是什么光景。 +see-also: mush-mud-muck-moo +see-also: connecting +see-also: codebases/pennmush +see-also: codebases/aresmush +see-also: codebases/evennia +see-also: clients/mudlet +--- + +我们说的*协作扮演*是指:你和另外一些人一起写一场戏,以角色的身份,用散文,而且基本上是实时的。 +除非游戏要求,否则没有人掷先攻。这是被“MUD”这个词遮住的那一块,而且它活得好好的。 + +## 去找这些代码库 + +这种文化生活在 TinyMUD 这一脉里,因为它的服务器是为造东西的人、而不是为杀东西的人造的。 + +- **[PennMUSH](/reference/codebases/pennmush)** 和 **[TinyMUX](/reference/codebases/tinymux)**—— + 两大 MUSH 服务器。你听说过的那些长期运营的扮演游戏,大多跑的是其中之一。 +- **[AresMUSH](/reference/codebases/aresmush)**——现代的,专为扮演而造, + 网页前端和场景记录是内建的一等功能,而不是某个人写的 softcode。 +- **[Evennia](/reference/codebases/evennia)**——一个 Python 框架,而不是一个做好的游戏。 + 建在它上面的游戏差别极大;较新的扮演游戏里有好几个是 Evennia。 +- **[MUCK](/reference/codebases/muck)**——自成一套文化,社交色彩很重; + 如果你要找的是同人圈那类世界,它值得了解。 + +你可以按其中任何一项来筛选目录:每个代码库页面都链接到我们实测出运行着它的那些游戏。 + +## 一个不会跟你作对的客户端 + +你并不非有一个不可——这里的每个游戏都说普通的 telnet,而你的操作系统里多半已经带了一个客户端。 +但对写戏来说,有三件事比在别处更要紧: + +1. **一条撑得住长段落的命令行。** 你会敲下好几句话的 pose(动作描述)。 +2. **日志记录。** 事后你会想要那场戏的记录。 +3. **UTF-8。** 名字里会有重音符号。 + +请看[客户端](/reference)一节。如果你使用屏幕阅读器,那里的能力对照表专门有一行; +我们没能对每个客户端都确定出答案,而没能确定的地方,表里写的是*未知*,而不是去猜。 + +## 第一晚实际上是什么光景 + +大多数扮演游戏都有一套**申请流程**:你以访客身份连上去,读一些帮助文件, +然后写一份角色设定交给管理组看。这可能花掉一天,也可能花掉两周。这不是为设门槛而设门槛—— +游戏正是靠它来维持一个连贯的共享设定——但这确实意味着,“登录就玩”很少是它的样子。 + +你去看的那一刻一个人都没有,在这里很正常;每个游戏页面上的活跃度网格比它的实时人数更有用: +一个每天傍晚有十五个人、凌晨四点一个都没有的游戏,是一个在错的时刻被看到的健康游戏。 + +## 本站无法告诉你的事 + +那里的人好不好。管理组公不公道。那个设定是不是你会喜欢的。我们实测的是服务器。 +这里没有评分,将来也不会有——那是一个刻意的决定,不是一处空缺。 diff --git a/content/reference/zh-Hans/orientation-connecting.md b/content/reference/zh-Hans/orientation-connecting.md new file mode 100644 index 0000000..50148ae --- /dev/null +++ b/content/reference/zh-Hans/orientation-connecting.md @@ -0,0 +1,42 @@ +--- +kind: orientation +slug: connecting +title: 如何连接 +summary: 一个主机、一个端口,加上 telnet。游戏页面上那个地址是什么意思,以及拿它来做什么。 +see-also: mush-mud-muck-moo +see-also: protocols/tls +see-also: protocols/charset +--- + +这里列出的每个游戏都在一个**主机加一个端口**上应答,底下的协议是 telnet—— +实际上就是一条裸的 TCP 连接,外加其上少量可选的协商。 + + telnet mush.pennmush.org 4201 + +这管用,而且在很多系统上它已经装好了。它同时也是一种很差的玩法:系统自带的 `telnet` +没有像样的本地回显控制,没有日志,没有历史记录,而且会把 ASCII 以外的一切弄乱。 +它是用来确认一个游戏是否还应答的合适工具,却不是用来消磨一个晚上的那个。 + +## 游戏页面上的地址告诉你什么 + +每个游戏页面都列出我们实测到的端点,并标出其中观测到 **TLS** 的那些。有 TLS 端口的游戏, +你可以加密连上去;这个端口号通常和明文的那个不一样。 + +一个游戏有好几个端口时,它们往往是通往同一个世界的不同路径,而不是不同的游戏。 +我们列出我们实测到的内容,不去猜哪一个才是正式的那个。 + +## 挑一个客户端 + +[客户端](/reference)一节里每个客户端都有一页,带一张能力对照表。装任何东西之前值得先看的三件事: + +- **它支持 UTF-8 吗?** 如果这个游戏不是只用英文,第一个晚上你就会碰上这个问题。 +- **它支持 TLS 吗?** 只有在游戏提供 TLS 时才有意义,不过现在已经有好几个提供了。 +- **如果你使用屏幕阅读器,这个项目有没有写明它支持屏幕阅读器?** + 这是各家客户端对照里最常缺席的一行,所以在我们这里它是第一行—— + 而在没有人确定出答案的地方,它写的是*未知*。 + +## 如果什么都没应答 + +一个不应答的游戏未必就没了。游戏会换主机,DNS 会失效,防火墙也有自己的主张。 +本站保留它实测过的每一个游戏——包括那些好几年前就不再应答的——并且每周继续叩门, +所以在下任何结论之前,该去看的地方是[归档](/archive)。 diff --git a/content/reference/zh-Hans/orientation-families.md b/content/reference/zh-Hans/orientation-families.md new file mode 100644 index 0000000..bd1bd27 --- /dev/null +++ b/content/reference/zh-Hans/orientation-families.md @@ -0,0 +1,74 @@ +--- +kind: orientation +slug: mush-mud-muck-moo +title: MUSH、MUD、MUCK、MOO——这些词是什么意思 +summary: 四个词对应四种传统,没有一个是题材类型。它们真正告诉你的是什么。 +see-also: collaborative-roleplay +see-also: connecting +see-also: codebases/pennmush +see-also: codebases/aresmush +see-also: codebases/muck +see-also: codebases/moo +see-also: codebases/evennia +--- + +这几个词,每一个命名的都是**一族服务器软件**,而不是一类游戏。这是关于它们最有用的一条认识, +也正是“这是 MUSH 还是 MUD?”这个问题为什么老被答得很糟: +老实的答案通常是*两者都是,而你想问的其实是文化*。 + +## MUD + +最老的一个词,如今也是最宽的一个。它起初是 *Multi-User Dungeon*——Bartle 和 Trubshaw +1978 年的那个游戏——到九十年代中期,它已经成了所有文字多人世界的统称。 + +狭义地用,它指的是 **DikuMUD 和 LPMud 这两脉**:围绕等级、战斗、装备, +以及一个由建造者事先写好、描述各个房间的区域文件搭起来的服务器。 +如果有人说“我玩 MUD”并且是有所指的,通常指的就是这个。 + +列表可以把每一脉单独显示给你看:[DikuMUD 的游戏](/games?lineage=DikuMUD)和 +[LPMud 的游戏](/games?lineage=LPMud)。 + +## MUSH + +*Multi-User Shared Hallucination*,出自 TinyMUD 一脉。它的定义性特征不是题材, +而是 **softcode(软代码)**:MUSH 服务器自带一门玩家在游戏内部使用的编程语言, +于是有建造权限的玩家可以创建房间、物件和行为,既不用碰源文件,也不用重启任何东西。 + +正是这一个设计决定造就了那套文化。MUSH 往往自动化系统很稀薄,而人力系统很密集—— +管理组主持的剧情、写出来的场景、申请流程——因为玩的人同时也是造的人。 + +PennMUSH、TinyMUSH、TinyMUX、RhostMUSH、CobraMUSH 和 AresMUSH 都属于这一脉, +而它们没有一个这么说:MSSP 里没有 `MUSH` 这个取值可供公布, +而除 PennMUSH 之外的全都根本不发布 MSSP。所以把它们归到一起是我们做的事, +而不是我们读到的事,这就是[MUSH 的游戏](/games?lineage=MUSH)无论出现在哪里都标着*推算*的原因。 + +## MUCK + +和 MUSH 一样是 TinyMUD 的后裔,有自己的 softcode(MUF,一门类 Forth 的语言), +以及一条深厚的社交世界与福瑞同人世界的传统。技术上与 MUSH 很接近; +文化上则区别到,两边都玩的人不会把它们说成是一回事——[MUCK 的游戏](/games?lineage=MUCK)。 + +## MOO + +*MUD, Object-Oriented*。“游戏自己编辑自己”这个想法最纯粹的表达:MOO 里几乎所有东西, +都是使用它的人在里面用 MOO 编程语言写出来的。LambdaMOO 是它的祖先, +而 MOO 历来在教育和研究领域和在游戏领域一样受欢迎。[MOO 的游戏](/games?lineage=MOO)。 + +## 那么,你实际上该问什么? + +有三个问题,比那个四字母的词管用得多: + +1. **有没有战斗,战斗是不是自动化的?** 这一条把 Diku/LP 一脉和 TinyMUD 一脉分开, + 比任何名字都可靠。 +2. **谁来建造?** 只有管理组,还是任何拿到建造位的人? +3. **玩法是约时间的还是随时随地的?** 是约好时间的场景和 pose 出来的扮演,还是登录就走? + +本站的列表能替你回答第一个问题的一部分:我们为一个游戏实测出的**代码库**, +会告诉你它的服务器出自哪一种传统,而**谱系**这个筛选项就是把那个答案变得可筛选。 +它没法告诉你文化,本页也不会假装它能。 + +关于这个筛选项还有一句提醒,因为总有人是在本页上第一次遇到它。代码库是实测的,谱系不是: +它是*我们*对一个游戏告诉我们的内容所作的归类,挂在它自己的标签下——**推算**—— +与*实测*和*自述*并列。凡是代码库没有一个无争议的父系的, +我们宁可把它排除在所有谱系之外,也不把它归到最接近的那一个下面; +而这样的游戏里有好几个用它们自己的话表示了同意,公布的是 `FAMILY Custom`。 diff --git a/content/reference/zh-Hans/protocol-atcp.md b/content/reference/zh-Hans/protocol-atcp.md new file mode 100644 index 0000000..42a9ab5 --- /dev/null +++ b/content/reference/zh-Hans/protocol-atcp.md @@ -0,0 +1,27 @@ +--- +kind: protocol +slug: atcp +title: ATCP +summary: GMCP 的前身。带外数据,载荷格式更松散,如今大体已被取代,仍有一直没把它删掉的服务器在继续协商它。 +protocol: ATCP +see-also: protocols/gmcp +see-also: protocols/msdp +see-also: clients/mudlet +--- + +ATCP——Achaea Telnet Client Protocol——是 telnet 选项 200,在 MUD 文本之外另发一份结构化数据的想法, +最早就是在这里被大规模用起来的。服务器发出一个模块名和一份载荷,客户端把它分发出去。 + +它的载荷格式比 [GMCP](/reference/protocols/gmcp) 的 JSON 松散,这基本上就是 GMCP 取代它的原因。 +如今支持 ATCP 的客户端,一般都把它标注为已废弃,并让你改用 GMCP。 + +## 它为什么还在 + +因为把它开着并不会弄坏什么。一台 2008 年实现了 ATCP、2014 年又加上 GMCP 的服务器,通常两个都还在协商; +而两个都支持的客户端,对方给哪个就用哪个。 + +对一个新的实现来说,没有理由选它。 + +## 我们实测的是什么 + +在我们观测到的握手中提供了 telnet 选项 200 的服务器。这里的数字低是意料之中的,它说的是年代,别的什么也不说。 diff --git a/content/reference/zh-Hans/protocol-charset.md b/content/reference/zh-Hans/protocol-charset.md new file mode 100644 index 0000000..757a705 --- /dev/null +++ b/content/reference/zh-Hans/protocol-charset.md @@ -0,0 +1,35 @@ +--- +kind: protocol +slug: charset +title: CHARSET +summary: RFC 2066 里用来商定编码的 telnet 选项。一个游戏中带重音符号的名字能完好走完全程,靠的就是它;而它缺席时,一些细微的故障也由此而来。 +protocol: CHARSET +home: https://www.rfc-editor.org/rfc/rfc2066 +see-also: protocols/ttype +see-also: connecting +see-also: codebases/tinymux +--- + +CHARSET 是 telnet 选项 42,由 RFC 2066 规定。一方给出一份字符集清单,另一方从中挑一个, +双方随后就字节如何映射到字符达成一致。 + +实际当中,这场协商要么落在 **UTF-8** 上,要么根本就没有发生。MUSH 家族协商它的比例明显高于 MUD 家族 +——TinyMUX、RhostMUSH 和 PennMUSH 都协商——这反映的是一群写散文、而且文中带名字的人。 + +## 没有它会怎样 + +客户端只能猜,而通常猜的不是 ASCII 就是 Latin-1。猜 ASCII,0x7F 以上的每个字节都会变成问号; +在 UTF-8 服务器上猜 Latin-1,每个带重音的字符都会变成两个标点。这两种故障看上去都像是游戏的错,其实都不是。 + +对爬虫来说,这件事会在一个很具体的地方咬人。我们自己的 telnet 库把当前编码默认设成 ASCII, +而这个默认值并不是摆设——对每一台从不协商 CHARSET 的服务器(也就是它们中的绝大多数), +每一个字节都是用它来解码的。正因如此,我们才特意给它预置了一个值。 + +## CHARSET 唯一够不到的地方 + +不管 CHARSET 最后谈成了什么,MSSP 的字段名和字段值都按 ASCII 解码,因为子协商是命令而不是文本, +而规范把 CHARSET 的适用范围限定在文本上。这大概算是合规的,同时也是有损的:一个 MSSP `NAME` 为 +`Café Noir` 的游戏,报出来是 `Caf? Noir`,而原始字节在我们能控制的任何环节看到它之前就已经没了。 + +如果你在本站某个自述字段里看到一个乱掉的字符,而游戏自己的输出里没有,原因就在这里, +并且这在我们这一侧无法还原。 diff --git a/content/reference/zh-Hans/protocol-gmcp.md b/content/reference/zh-Hans/protocol-gmcp.md new file mode 100644 index 0000000..02409a4 --- /dev/null +++ b/content/reference/zh-Hans/protocol-gmcp.md @@ -0,0 +1,38 @@ +--- +kind: protocol +slug: gmcp +title: GMCP +summary: Generic Mud Communication Protocol——与文本并行的结构化 JSON 消息,也是当下多数客户端所面向的带外通道。 +protocol: GMCP +home: https://www.mudhalla.net/tintin/protocols/gmcp/ +see-also: protocols/msdp +see-also: protocols/atcp +see-also: clients/mudlet +--- + +GMCP 是 telnet 选项 201。一旦协商成功,服务器就可以**带外发送结构化数据**: +一个包名加一份 JSON 载荷,与文本走在同一条流里,却不是文本的一部分。 + +`Char.Vitals { "hp": 412, "maxhp": 500 }` 是最典型的例子。客户端可以据此驱动一条血条, +而不必从字里行间去抠数字,这正是它的全部意义所在——靠模式匹配文本搭起来的状态显示, +在游戏改动提示符的那一天就会坏掉,建在 GMCP 上的则不会。 + +包的命名空间是约定俗成的,而不是标准化的。`Char`、`Room`、`Comm` 和 `Client` 用得很广; +再往外,各游戏需要什么就自己发明什么,而客户端一般得有人告诉它某个游戏会发些什么。 + +## 它为什么取代了 ATCP + +GMCP 是 [ATCP](/reference/protocols/atcp) 的后继者,后者干的是同一件事,只是载荷格式更松散。 +JSON 就是那个改进,而这场迁移到 2010 年代中期基本已经完成。一个游戏两个都支持并不稀奇; +一个新游戏只支持 ATCP 才叫稀奇。 + +## 我们实测的是什么 + +一个游戏被算进这里,条件是**它的服务器在我们观测到的一次握手中提供了 GMCP**。这与某个游戏的 MSSP 里写着 +`GMCP 1` 是两种不同的说法——而这个圈子里大多数协议表恰恰是建立在后者之上的——两者经常对不上。 + +有一条来自我们自身历史的实测说明:有那么一段时间,凡是同时协商了 +[MCCP](/reference/protocols/mccp) 的服务器,我们都看不到它们的 GMCP, +因为我们的 telnet 库协商了压缩却从不解压,压缩标记之后的一切对我们来说都是噪声。 +我们调查过的服务器里,至少有一台其实一直都在说 GMCP。如果本页上的某个数字, +对一个你很熟悉的家族来说低得不对劲,第一个该怀疑的就是这类缺陷——怀疑我们,而不是怀疑他们。 diff --git a/content/reference/zh-Hans/protocol-mccp.md b/content/reference/zh-Hans/protocol-mccp.md new file mode 100644 index 0000000..8951cd0 --- /dev/null +++ b/content/reference/zh-Hans/protocol-mccp.md @@ -0,0 +1,36 @@ +--- +kind: protocol +slug: mccp +title: MCCP +summary: 流压缩。开销小、部署广,也是本项目历史上最有教益的那个 bug 的来源。 +protocol: MCCP +home: https://www.mudhalla.net/tintin/protocols/mccp/ +see-also: codebases/rom +see-also: codebases/dikumud +see-also: protocols/gmcp +--- + +MCCP 用 zlib 压缩服务器到客户端的数据流。版本 1 是 telnet 选项 85,实际上只剩历史意义; +**版本 2** 是选项 86,也是现代服务器会协商的那一个。服务器发出 `IAC SB MCCP2 IAC SE` 之后, +随后的每一个字节都属于同一条连续的 zlib 流。 + +在一个文本协议上,这是实打实的节省——MUD 的输出压缩率极高——而且它在 Diku 和 LP 家族里很常见: +我们调查过的代码库中,大约三分之一会协商它。 + +## 它的故障模式,以及这在本站为什么要紧 + +一个协商了 MCCP2 却不去解压数据流的客户端,会**从压缩标记开始收到一片二进制乱码**。不是报错,也不是断线: +连接画面变成一整墙替换字符,而在它之后的一切——`WHO` 的回复、后面的任何 MSSP、整场会话——统统丢失。 + +这不是假想。我们自己的 telnet 库干的就是这件事。它协商了这个选项,触发了自己的“压缩已启用”回调, +却一个字节都没解压过。那份载荷用一个现成的 zlib 调用就干干净净地解开了,正是这一点让事情毫无疑义: +对的是服务器,错的是我们。我们调查的三十八个代码库里有十三个受到影响, +而在这段时间内,我们无法观测这些服务器在压缩开始*之后*协商了什么—— +所以我们对它们能力的记录是偏低的。 + +这个问题已在上游修复。还有一个后续缺陷——解压器是每次读取都重建,而不是随连接一直保留, +于是在一张很大的连接画面读到一半时失败——已经提交且仍未关闭,它影响的是最大那些画面的尾部。 + +读者应该从这里带走两件事。**本页上的协议数字,既是对这个圈子的实测,同样也是对我们爬虫的实测**, +凡是我们知道它错过的地方,我们都会说出来。另外,如果你在写客户端:协商 MCCP 很容易, +而正确地解压才是工作量真正所在的地方。 diff --git a/content/reference/zh-Hans/protocol-msdp.md b/content/reference/zh-Hans/protocol-msdp.md new file mode 100644 index 0000000..6bd8a91 --- /dev/null +++ b/content/reference/zh-Hans/protocol-msdp.md @@ -0,0 +1,26 @@ +--- +kind: protocol +slug: msdp +title: MSDP +summary: Mud Server Data Protocol——和 GMCP 干同一件事,用的是一种紧凑的二进制编码,外加一套 GMCP 所没有的发现机制。 +protocol: MSDP +home: https://www.mudhalla.net/tintin/protocols/msdp/ +see-also: protocols/gmcp +see-also: clients/tintin +see-also: clients/blightmud +--- + +MSDP 是 telnet 选项 69,它解决的问题与 [GMCP](/reference/protocols/gmcp) 相同: +在文本之外发送结构化数据,好让客户端不必从字里行间去抠数字。 + +差别有两处。MSDP 的编码是**二进制且紧凑的**——变量和取值用单个控制字节来标记,而不是包进 JSON—— +并且 MSDP 定义了一套**发现**用的对话:客户端可以用 `LIST` 索取 `COMMANDS`、`REPORTABLE_VARIABLES` +等等,从而被告知某个游戏支持些什么。GMCP 没有对等的东西,这就是 GMCP 客户端一般得逐个游戏去配置的原因。 + +实际上,GMCP 在采用率上赢了,而 MSDP 在那些实现过它的服务器和客户端里留存下来,常常与 GMCP 并存。 + +## 我们实测的是什么 + +一个游戏被算进这里,条件是它的服务器在我们观测到的一次握手中提供了 MSDP。和本节里的每一个数字一样, +那是一次正面的观测,而剩下的部分并不是它的反面——一个没被算进来的游戏,可能没有实现 MSDP, +也可能只是它的握手还没被我们读到过。 diff --git a/content/reference/zh-Hans/protocol-msp.md b/content/reference/zh-Hans/protocol-msp.md new file mode 100644 index 0000000..7cd4398 --- /dev/null +++ b/content/reference/zh-Hans/protocol-msp.md @@ -0,0 +1,34 @@ +--- +kind: protocol +slug: msp +title: MSP +summary: MUD Sound Protocol——服务器点名一个声音文件,客户端把它放出来。古老、简单,也很容易与另外两样东西弄混。 +protocol: MSP +home: https://www.zuggsoft.com/zmud/msp.htm +see-also: protocols/mxp +see-also: clients/vipmud +--- + +MSP 让服务器可以请求客户端播放一段声音:一条用方括号括起来的指令,点明文件、音量、重复次数, +以及在客户端手上没有这个文件时用来取回它的 URL。它在 telnet 选项 90 上协商, +而那些从不协商任何东西的服务器,也可以把它带内塞在文本流里发出来。 + +它确实古老,也确实仍在使用——文字游戏里的环境音,效果比听起来要大; +而对于靠客户端的音频提示而不是靠画面来玩的玩家,它不只是装饰。 + +## 它不是的三样东西 + +本节里的客户端对照表在这里必须格外小心,值得把原因写下来: + +- **MCMP**——Mud Client Media Protocol——是另一个协议,干着类似的活。至少有一个客户端实现了 MCMP + 而没有实现 MSP,把其中一个读成另一个,就会在表里放进一句谁也没说过的话。 +- **客户端自己的“播放一段声音”脚本调用**不是 MSP。它是在脚本这么说的时候播放一个本地文件; + MSP 则是服务器告诉客户端该放什么。 +- **以捆绑插件形式提供的支持,值得照实写明。** 有一个客户端的 MSP 支持是以插件形式随附的, + 而该插件明确不做任何 telnet 协商,因此它在带内发送 MSP 的服务器上可用, + 在期待先协商的服务器上则不可用。 + +## 我们实测的是什么 + +提供了 telnet 选项 90 的服务器。由于 MSP 经常不经协商就带内发出,这个数字低估了实际的部署情况, +低估的幅度我们无法估计——这是一次握手所能看到的范围的局限,而不是关于这个协议的结论。 diff --git a/content/reference/zh-Hans/protocol-mssp.md b/content/reference/zh-Hans/protocol-mssp.md new file mode 100644 index 0000000..4d2f52e --- /dev/null +++ b/content/reference/zh-Hans/protocol-mssp.md @@ -0,0 +1,52 @@ +--- +kind: protocol +slug: mssp +title: MSSP +summary: Mud Server Status Protocol——一个游戏如何向爬虫讲述自己。它报告的一切都是自述而非实测,本站把这两者分得清清楚楚。 +protocol: MSSP +home: https://www.mudhalla.net/tintin/protocols/mssp/ +see-also: protocols/gmcp +see-also: codebases/dikumud +see-also: codebases/pennmush +--- + +MSSP 是 telnet 选项 70。爬虫发送 `IAC DO MSSP`,支持它的服务器就回一张描述自己的名值对表格—— +名称、在线人数、代码库、运行时长、主机名、端口、题材,以及它愿意公布的其他任何东西。 + +它是这个圈子里最接近“机器可读的目录条目”的东西,也是好几个目录站点得以存在的原因。 + +## MSSP 报告里的每一项都是一句断言 + +本站与所有既有站点的分歧就在这一点上。一份 MSSP 报告是游戏在*告诉你*它自己的情况。 +MSSP 表里的 `GMCP 1` 意味着有人在某个配置文件里敲了个 `1`,也许是在 2011 年。 +它不是服务器提供 GMCP 的证据,而两者对不上的次数,多到足以让人感兴趣。 + +所以来自 MSSP 的事实在这里标为**自述**;凡是同一项事实我们也能实测的——比如一项能力, +去看这个选项是否真的被协商了——两者都会并排显示,各自带着一个时间。一个游戏的 MSSP +自述支持 GMCP 已有六年,却从未在任何一次握手中提供过它,这是个值得知道的事实,而且别处找不到。 + +有一个字段我们刻意完全不予采信:`CREATED`。它只是一行手敲的文字, +把它计入任何东西,都会让那样东西轻易地被人做手脚。 + +## 谁会应答它 + +MSSP 是 **Diku 和 LP** 那边的答案。在我们自己那份涵盖 38 个代码库的调查中, +有 28 个通过 MSSP 公布在线人数,7 个通过登录画面上的 `WHO` 公布,两样都做的只有 2 个—— +这两个家族几乎完全不相交。AresMUSH、TinyMUX、MUCK、RhostMUSH、CobraMUSH 和 TinyMUSH +完全不提供 MSSP。 + +这就是要探测四个层次而不是一个层次的实证理由:**只建立在 MSSP 之上的爬虫, +看不见 MUSH 家族的大部分**,而那是这个圈子里很大的一块,也是本站预期读者中的大多数。 + +## 主动索取,不要干等 + +有相当多完全支持 MSSP 的服务器从不主动给出它——它们会应答 `IAC DO MSSP`,除此之外一言不发。 +于是,一个上来先发 `IAC WILL NAWS` 然后等着的爬虫,会把这些游戏报成什么都没公布, +而这是拿爬虫自己的沉默去下一句关于服务器的断言。我们在连接时就发送 `IAC DO MSSP`。 + +## 纯文本形式 + +还有一种更老的变体:客户端在登录画面上直接发送 `MSSP-REQUEST` 这一行字面文本。我们实测过: +在试过的二十个游戏里,有三个作了应答——而这三个同样也应答 telnet 选项 70, +所以它没能触及任何选项 70 触及不到的东西。有八台服务器把这个请求读成了一个**角色名**并如此回复, +白白花掉了一个陌生人被允许的登录尝试次数中的一次。我们不发送它。 diff --git a/content/reference/zh-Hans/protocol-mxp.md b/content/reference/zh-Hans/protocol-mxp.md new file mode 100644 index 0000000..b4203ee --- /dev/null +++ b/content/reference/zh-Hans/protocol-mxp.md @@ -0,0 +1,36 @@ +--- +kind: protocol +slug: mxp +title: MXP +summary: MUD eXtension Protocol——嵌在文本流里的类 HTML 标记,带来可点击的链接、图片和表单。规范写得详尽,实现却参差不齐。 +protocol: MXP +home: https://www.zuggsoft.com/zmud/mxp.htm +see-also: protocols/pueblo +see-also: clients/mushclient +see-also: clients/mudlet +--- + +MXP 在服务器发出的文本里嵌入了一门小巧的、类似 HTML 的标记语言:`` 表示一条可点击的命令, +`` 表示一个链接,另有颜色和字体元素,以及一套让服务器自定义标签的机制。 +它在 telnet 选项 91 上协商。 + +它的设计难题是固有的,也很有意思:标记与文本走在同一条流里,所以服务器必须小心那些*看起来*像标记的文本, +而客户端必须小心自己会渲染什么。MXP 定义安全级别正是为了这个原因—— +夹在另一个玩家的一行聊天里送来的标签,和服务器自己发出的标签不是一回事。 + +## 人们想要它,是为了可点击 + +MXP 实际用途的大部分,就是把 `north` 和物品名变成可以点的东西。对新玩家来说这个差别相当大, +也正因如此,这个协议尽管复杂,却还是不断有人实现。 + +## Pueblo 是另外那一个 + +[Pueblo](/reference/protocols/pueblo) 比 MXP 更早,用一种不同的、字面上更像 HTML 的做法干着类似的活。 +支持其中一个的客户端往往并不支持另一个,而在读功能清单时这两者很容易混淆—— +本节里的客户端对照表就得小心提防这个错误。 + +## 我们实测的是什么 + +在我们观测到的握手中提供了 telnet 选项 91 的服务器。MXP 被协商的频率低于那些带外协议, +一部分原因是它的价值有很大一块,是由那些干脆不协商、直接把标记发出去碰运气的服务器实现的—— +而那些我们看不见。 diff --git a/content/reference/zh-Hans/protocol-pueblo.md b/content/reference/zh-Hans/protocol-pueblo.md new file mode 100644 index 0000000..f4bbaff --- /dev/null +++ b/content/reference/zh-Hans/protocol-pueblo.md @@ -0,0 +1,28 @@ +--- +kind: protocol +slug: pueblo +title: Pueblo +summary: 更早的那套“在 MUD 里用 HTML”的方案,出自同名的客户端。MUSH 那一侧的客户端仍在支持它,它也经常被人与 MXP 弄混。 +protocol: PUEBLO +home: https://pueblo.sourceforge.net/ +see-also: protocols/mxp +see-also: clients/beipmu +--- + +Pueblo 出自九十年代中期同名的那个客户端,它增强 MUD 文本的路子很直接:让服务器发 **HTML**, +让客户端把它渲染出来。服务器在连接时用一行文字宣告自己支持 Pueblo,客户端作出回应, +从此这条流里就可以携带标记了。 + +它传到这个圈子里 MUSH 那一侧的程度大于 MUD 那一侧,而支持它的 MUSH 服务器一般至今仍在支持。 + +## 它不是 MXP + +[MXP](/reference/protocols/mxp) 是更晚的那套方案,也是实现得更广的那套。两者干的活类似, +彼此并不兼容,而把某个客户端的 Pueblo 支持读成 MXP 支持——或者反过来—— +是编制客户端对照时最容易犯的一个错。本节的客户端页面正因如此把它们分开列; +当一个项目只记载了其中一个而没有记载另一个时,另一个就写*未知*。 + +## 我们实测的是什么 + +Pueblo 的握手不是通常意义上的 telnet 选项,所以我们能观测到的范围比那些协商式协议要窄; +这里的数字低,应当读作一句关于我们可见范围的话,而不是关于部署情况的话。 diff --git a/content/reference/zh-Hans/protocol-tls.md b/content/reference/zh-Hans/protocol-tls.md new file mode 100644 index 0000000..8f56862 --- /dev/null +++ b/content/reference/zh-Hans/protocol-tls.md @@ -0,0 +1,35 @@ +--- +kind: protocol +slug: tls +title: TLS +summary: 加密的连接。通常是另开一个端口,而不是协商式的带内升级;它也是本站唯一一项靠亲自连上去、而不是靠开口询问来核实的能力。 +protocol: TLS +see-also: connecting +see-also: protocols/charset +see-also: clients/potato +--- + +telnet 是明文的。你发给一个 MU\* 的一切——包括你的密码——都以路径上任何东西都读得懂的形式穿过网络, +除非这个游戏提供 TLS。 + +在这个圈子里,TLS 几乎总是意味着**另有一个从第一个字节起就说 TLS 的端口**,而不是带内升级。 +明文端口在 4201、TLS 端口在 4202,是常见的形态。确实存在一种协商式的变体, +但它罕见到至少有一个客户端的文档明确写着不支持。 + +## 游戏页面为什么单独标注它 + +TLS 是本站唯一一项靠*亲自做一遍*来确立的能力:一个端点被标为 TLS, +是因为我们对它完成了一次 TLS 握手。这里没有询问,也没有可供自述的字段, +这让它成为整个目录里最干净的一项实测。 + +这也是为什么一个游戏的 TLS 端口和它的明文端口是作为两个独立端点分别列出的,而不是合并在一起。 +它们是对不同的东西所作的不同实测。 + +## 实用建议 + +如果你玩的游戏提供了 TLS 端口,就用它。如果没有而你又在意,那就去问——对管理员来说这工作量不大, +而它之所以还没普及,多半是因为没人开口,而不是因为有人反对。 + +在依赖它之前,先确认你的客户端支持它。[客户端](/reference)一节里有好几个是支持的; +至少有一个记载的是改用外部 `stunnel` 进程的变通办法,这办法可行, +但要配置的东西比大多数人愿意折腾的要多。 diff --git a/content/reference/zh-Hans/protocol-ttype.md b/content/reference/zh-Hans/protocol-ttype.md new file mode 100644 index 0000000..4f25839 --- /dev/null +++ b/content/reference/zh-Hans/protocol-ttype.md @@ -0,0 +1,41 @@ +--- +kind: protocol +slug: ttype +title: TTYPE and MTTS +summary: 客户端如何告诉服务器自己是什么、能做什么——包括在它愿意开口的时候,说出正有人在使用屏幕阅读器。 +protocol: TTYPE +home: https://www.mudhalla.net/tintin/protocols/mtts/ +see-also: protocols/charset +see-also: clients/tintin +see-also: clients/blightmud +--- + +TTYPE 是 telnet 选项 24,出自 RFC 1091:服务器问客户端它是什么终端,客户端作答。 +历史上这个答案是 `VT100` 或 `ANSI`。 + +**MTTS**——Mud Terminal Type Standard——在它之上叠了一层约定。客户端回答三次:它的名字、 +它的终端类型,然后是 `MTTS `,其中的各个比特位自述它具备哪些能力。256 色、真彩色、 +UTF-8、MNES、走带外的 MSP——以及很值得注意的 **`MTTS_SCREEN_READER`**。 + +## 屏幕阅读器那一位 + +最后这一项值得停下来说一说,因为在这个圈子的协议栈里,只有这一处把无障碍当作一等的概念。 + +设置了这一位的客户端,是在告诉服务器有人正在使用屏幕阅读器;注意到这一点的服务器可以随之调整: +不再输出 ASCII 图画,去掉房间描述外面那圈装饰性的制表边框,改变表格的排版方式。 +[TinTin++](/reference/clients/tintin) 和 [Blightmud](/reference/clients/blightmud) 都会声明它, +[Mudlet](/reference/clients/mudlet) 有一个对应的设置项。 + +至于某个具体的游戏是否真的据此做了什么,那是另一个问题,而且不是本站能实测的问题—— +我们没法去问一台服务器,它会做出什么不一样的处理。 + +## 爬虫在这里该尽的本分 + +爬虫要通过 TTYPE 表明自己的身份,它也应该这么做。我们的爬虫就这么做,并附上一个说明页地址, +好让翻看日志的管理员能弄清是谁一直在连他们的游戏,以及该怎么让我们停下。 +一个只回答 `ANSI`、别的什么都不说的爬虫,是设计上就匿名的,而这没有什么好理由。 + +## 我们实测的是什么 + +与我们协商了 TTYPE 的服务器。要注意,这是少数几个由*我们*作为被询问一方的选项之一, +所以这里的数字,数的是那些愿意开口来问的服务器。 diff --git a/docs/2026-08-17-find-page-qa.md b/docs/2026-08-17-find-page-qa.md new file mode 100644 index 0000000..0396726 --- /dev/null +++ b/docs/2026-08-17-find-page-qa.md @@ -0,0 +1,342 @@ +# Find a game — mockup parity QA + +An audit of `/find` on `design/measured-once` against the **third** handoff bundle +(`Mu-index accessibility audit (2).zip` → `design_handoff_find_page/`), which is scoped to this one +page and supersedes the second bundle where they overlap. Same shape as +[`2026-08-17-mockup-parity-qa.md`](2026-08-17-mockup-parity-qa.md): numbered deltas with what the +mock says, what ours does as measured, and a severity; then the conflicts. + +**Parts one to four were written before the work; part five records what shipped.** Everything in +part two is now built except where part five says otherwise. + +## Method + +- Bundle extracted to `/tmp/mui_handoff3/`; a copy of `MU Index Find Page.dc.html` with + `showAnnotations` defaulted to `false` served over `python3 -m http.server` and rendered in + Playwright's Chromium at 1440 / 1024 / 768 / 430. +- Ours at `http://127.0.0.1:5199/find`, both themes, same four widths, on the fixture — so every + page carries the demo banner, which CLAUDE.md requires and which is not a delta. Fixture counts + are small (6 games); the *shape* of each control is what is compared, never the numbers. +- Every value below is a `getComputedStyle` / `getBoundingClientRect` reading. +- `?plain=1` read as text; querystring behaviour probed with `curl`. + +--- + +# Part one — what the third bundle changed + +`_ds/` (tokens, `styles.css`, manifest, adherence config, fonts) and `support.js` are **byte-identical** +to the second bundle — `md5sum` matches on all ten files. The design system did not move. What +changed is the README and one new screen. + +| # | Change | Detail | +|---|---|---| +| Δ1 | **Find gets a screen and a review of its own** | Bundle 2 mocked home, games and game detail and gave Find four rows in a copy table. Bundle 3 is a single-page handoff with eight findings (F1–F8), a working prototype, a layout spec and a verification list. Everything below Δ7 is new material, not a restatement. | +| Δ2 | **Both intro paragraphs are now deleted, not rewritten** | Bundle 2 rewrote the lede to `Six optional questions. Every number is a count of games we measured, not an estimate.` and offered `Answers filter; they never rank.` "or cut". Bundle 3 deletes both outright — the live count is meant to make the point by demonstration. **We shipped bundle 2's version of both strings**, so both are now superseded. | +| Δ3 | **`uncounted` is reversed** | Bundle 2: `quiet is fine — we can reach it, and could not count` → **`uncounted` *(match the listing)***. Bundle 3: → **`reachable, but we can't count`**. A direct reversal of the string we shipped, and of a decision recorded in `FindAGame.razor`'s own comment. See §X6. | +| Δ4 | **The TLS gloss grew, and TLS folds into the protocol list** | Bundle 2: `TLS — handshake completed by us`. Bundle 3: `TLS — encrypted, handshake completed by us`, as row three of one uniform list, not a separate control. | +| Δ5 | **The acronym fix hardens** | Bundle 2 offered "add three words each **or** move the set behind an advanced disclosure". Bundle 3 removes the choice: every option reads *name · three-word gloss · count*, one shape per question. | +| Δ6 | **`?plain=1` is still described as absent from `/find`** | True of production in Aug 2026, stale for this branch — we shipped it. The requirement it carries is new though: plain must carry the `unknown` options and the count. | +| Δ7 | **New in bundle 3, with no antecedent in bundle 2** | The intersection count and the sticky panel (F1); the binding-answer "loosen" button (F2); `unknown` as a selectable option carrying its count (F3); grouping declared free text and badging the question `◆ derived` (F4); the long-tail collapse (F5); inverting the dark-games default (F7); the submit-hands-off-to-the-listing model and the shared vocabulary it needs (F8); single-select on every question including the client one; the layout spec — 24/14/24 rhythm, the `` padding trap, `overflow: clip` vs `hidden`, 310px panel, 40px option chips. | + +--- + +# Part two — the deltas + +Severity: **blocker** = the page makes a claim it cannot support, or an affordance is unreachable · +**major** = the redesign's premise or the site's own visual language is missing · +**minor** = a measurable gap that reads as polish. + +## L — Layout and chrome + +The Find page was not touched by either of the two earlier passes. It is the only page left on +**user-agent default form styling**. + +| # | Item | Mock | Ours (measured) | Sev | +|---|---|---|---|---| +| L1 | **Fieldsets are unstyled** | `border: 0`, questions separated by a 1px `--border-soft` hairline | `border: 2px groove rgb(107,107,107)` in dark, `rgb(239,239,239)` in light — the UA default, drawn full-bleed at 1394px | major | +| L2 | Fieldset padding | `0 24px 24px` | `4.9px 10.5px 8.75px`, plus a UA `margin: 0 2px` | major | +| L3 | **Vertical rhythm** | 24 rule→heading · 14 heading→options · 24 options→rule (measured on the mock as 24 / 14 / 25-incl-hairline) | **0 / 6.9 / 12.8** | major | +| L4 | Question separation | 1px hairline, full column width | fieldsets are flush: measured gap between consecutive fieldsets is **0** at every width; the groove border is the only separator | major | +| L5 | Legend typography | 16px / 600, `--text` | **14px / 400**, `padding: 0 2px` — a legend, never promoted to a heading | major | +| L6 | Legend padding | `24px 0 14px` on the legend itself (the README's warning: padding on the fieldset does not move it) | `0 2px` | major | +| L7 | No card, no two-column shell | one card, `grid-template-columns: minmax(0, 1fr) minmax(0, 310px)`, `gap: 0`, `align-items: stretch` | single flow column, `form.wizard` is `display: block`, `max-width: none`, spanning the full 1394px gutter-to-gutter | major | +| L8 | Footer row | `all games · random game · plain text` on `--surface-3` with a top hairline | a bare `plain text` link, no rule, no siblings | minor | +| L9 | `app.css` has no rule for this page | — | `fieldset.facets` and `.facet` are styled for the listing panel; **there is no selector for `form.wizard`, its `fieldset`, or its `legend`** anywhere in the 2394 lines | major | + +## N — The count panel (the centre of the redesign, entirely absent) + +| # | Item | Mock | Ours (measured) | Sev | +|---|---|---|---|---| +| N1 | **Intersection count** | mono 40px / 600, `tabular-nums`, accent, with the noun beside it at 13.5px | **does not exist**. Every number on the page is marginal, which is exactly F1's complaint | blocker | +| N2 | Sticky panel | outer cell carries `--surface-3` + left hairline at full row height (measured 310×1094 at 1440); inner wrapper `position: sticky; top: 20px`, `padding: 24px 22px`, `gap: 14px` | **0 elements with `position: sticky`** inside `main` | blocker | +| N3 | Kicker | `MATCHING ALL ANSWERS`, 9px, `letter-spacing: 1.44px`, uppercase, faint | absent | major | +| N4 | Sub-line | `of 515 known · 2 answers given`, 12px faint | absent | major | +| N5 | Answer chips | one per given answer, radius 99px, each with `aria-label="clear answer to: "` | absent | major | +| N6 | Loosen button | `× drop "Historical" → 4 games` when the count is small; the binding answer is the given answer with the smallest marginal count | absent. See §X2 — the number on it may not be estimated | major | +| N7 | `clear all answers` | plain link under the CTA | absent | minor | +| N8 | Submit | `Show these 19 games` — count in the label — 265×42, 14px/600, accent tint + `--accent-ring` + `--glow-sm`, radius 9 | `Show me the games`, **163×37**, `padding: 8px 14px`, **14px/400**, `rgb(29,33,37)` on dark, radius 9 | major | +| N9 | Live region | count in `aria-live="polite"`, debounced, announced as a sentence | **0 elements with `aria-live`** on the page. See §X3 — with no JS the round-trip replaces the live region rather than needing one | major | +| N10 | The panel's first line aligns with the first question's heading | `padding-top: 24px` on the sticky wrapper matching the legend | n/a — no panel | minor | + +## Q — Questions and options + +| # | Item | Mock | Ours (measured) | Sev | +|---|---|---|---|---| +| Q1 | **Option control** | `aria-pressed` buttons, `min-height: 40px`, `padding: 8px 14px`, radius 9px, laid out `flex-wrap: wrap; gap: 10px 8px` | native radios in `display: inline` labels; **19px tall**, `padding: 0`, `min-height: 0` — under half the 40px target and well under 44px | major | +| Q2 | **Three questions are `` × 3, measured **137×32 at every width including 430**, 13px, radius 6px. Two option shapes in one form, and a 32px target | major | +| Q3 | **`unknown` is not offered** | `unknown (395)` is a real selectable option in genre, kind and language, carrying its count | `FindAGame.razor:249` filters `.Where(v => !v.IsUnknown)`. **The query layer already supports it** — verified `/games?genre=~unknown&plain=1` returns a filtered listing with the row marked selected. The page declines to offer a filter the site can already apply | blocker | +| Q4 | Provenance badge per question | `● measured` / `◇ declared` / `◆ derived grouping` on every legend | none. `FacetWords.Evidence` and `FacetEvidence` already produce these three words for the listing panel | major | +| Q5 | Counts on every question | activity `199 / 229 / 280`, dark `493`; every option except "any" carries a number | activity and dark options carry **no counts at all**; genre/kind/language carry them inside the `

@link.Label

+

@link.Label

} } @if (!Catalogue.IsMeasured) { -

@SubmitCopy.NoCatalogue

+

@SubmitCopy.NoCatalogue(Tag)

} } @if (Catalogue.IsMeasured) {
-
+ - + - @SubmitCopy.HostHint + @SubmitCopy.HostHint(Tag) - + - +
@if (!Plain) {
-

What happens to an address

+

@L("submit.what.heading")

    - @foreach (var point in SubmitCopy.Points) + @foreach (var point in SubmitCopy.Points(Tag)) {
  1. @point
  2. }
-

plain text

+

@L("a11y.plainText")

} } @@ -139,10 +140,17 @@ else // Hidden means submitted and unclaimed — the state whose only exit is a claim, so that // is what the link offers. Everything else gets its public page. link = game.SubmittedAt is not null && !game.IsClaimed - ? SubmitLink.Claim(game.Slug) + ? SubmitLink.Claim(game.Slug, Tag) : SubmitLink.Game(game.Slug); } - Answer = SubmitCopy.Answer(outcome, SubmitLinks.Address(Host, Port), link); + Answer = SubmitCopy.Answer(outcome, SubmitLinks.Address(Host, Port), link, Tag); } + + /// The locale this page is being answered in. + [CascadingParameter] private HttpContext? Http { get; set; } + + private string Tag => Http.LocaleOf().Tag; + + private string L(string id) => Messages.For(Tag, id); } diff --git a/src/MUI.Web/Components/PlainText.cs b/src/MUI.Web/Components/PlainText.cs index 0085c40..8e63282 100644 --- a/src/MUI.Web/Components/PlainText.cs +++ b/src/MUI.Web/Components/PlainText.cs @@ -1,5 +1,6 @@ using System.Text; using MUI.Catalog; +using MUI.Web.Localization; namespace MUI.Web.Components; @@ -15,20 +16,51 @@ namespace MUI.Web.Components; /// /// It renders from the same view models the graphical pages use, which is what bounds its /// maintenance cost — the main pages are these with graphics added, not second documents that have -/// to be kept in step. Nothing here is wider than 80 columns and every state is a word, never a -/// glyph, a colour or a cell shape. +/// to be kept in step. No prose here is wider than 80 columns — addresses excepted, see +/// — and every state is a word, never a glyph, a colour or a cell shape. /// /// public static class PlainText { - /// Nothing this renderer writes exceeds this, because text browsers are 80 wide. + /// + /// No prose this renderer writes exceeds this, because text browsers are 80 wide. + /// + /// + /// URLs are the one exception, and are exempt on purpose. The cap used to be documented + /// as covering everything, which was not true and could not be made true: an address is printed + /// whole because a wrapped one is not clickable in the browsers this surface exists for, and a + /// find query with six answers in it is longer than eighty columns on its own. The choice is + /// between a line a reader can follow and a line a reader can use, and for an address it is the + /// second. Everything that is not an address wraps — — and + /// NoPlainLineIsWiderThanEightyColumns enforces exactly that split. + /// public const int Columns = 80; + /// + /// An address this surface prints, in the locale it is printing. + /// + /// + /// + /// These are the addresses on this surface — there are no anchors here, so a path is + /// printed for a reader to type, follow in a text browser or paste to somebody. That makes them + /// links in every sense that matters and subject to the same rule: a German reader who copies + /// /g/ashen-court off /de/games?plain=1 must not arrive in English, or the mirror + /// is telling them something the page it mirrors does not. + /// + /// + /// A query-only address is not passed through here and does not need to be: ?window=30d& + /// plain=1 means this page, asked differently in every language, and the reader is + /// already on the localized one. + /// + /// + private static string Path(string tag, string address) => LocaleRouting.Link(tag, address); + public static string Render( GamePage page, DateTimeOffset now, ReachSummary? reach = null, - TrendSeries? trend = null) + TrendSeries? trend = null, + string tag = Locales.SourceTag) { var b = new StringBuilder(); var s = page.Summary; @@ -54,30 +86,38 @@ public static string Render( b.AppendLine(); - // Every state spelled as a word. "Unknown" is written out rather than left blank, because a + // Every state spelled as a word. The absence is written out rather than left blank, because a // blank reads as zero to a human exactly as it does to a parser — and the count says how it // was obtained here as it does on the listing, or this page is the less honest of the two. + // + // The absence names no cause, for the reason the graphical hero's does not: a null count + // covers an unmeasured game, a probe that answered with nothing countable, and a count older + // than the window, and this surface can no more tell them apart than that one can. b.AppendLine((s.PlayersNow is { } n - ? $"Players now: {n} {Label(s.PlayersNowProvenance, now)}" - : "Players now: unknown (no count could be measured)").TrimEnd()); + ? $"{Say(tag, "game.plain.playersNow", ("count", n))} {Label(tag, s.PlayersNowProvenance, now)}" + : Say(tag, "game.plain.playersNoCount")).TrimEnd()); if (page.ReachableFraction is { } r) { - b.AppendLine($"Reachable: {Wording.Percent(r)} of the last 90 days"); + b.AppendLine(Say( + tag, + "reach.plain.fraction", + ("percent", Wording.Percent(r)), + ("days", ReachSeries.WindowDays))); } if (page.LongestOutage is { } o) { - b.AppendLine($"Longest outage: {Wording.Duration(o)}"); + b.AppendLine(Say(tag, "reach.plain.longestOutage", ("duration", Wording.Duration(o)))); } - AppendActivity(b, page.Activity); - AppendTrend(b, trend); - AppendReachable(b, reach); - AppendCapabilities(b, page); - AppendDeclared(b, page, now); - AppendConnectScreen(b, page); - AppendChanges(b, page); + AppendActivity(b, page.Activity, tag); + AppendTrend(b, trend, tag); + AppendReachable(b, reach, tag); + AppendCapabilities(b, page, tag); + AppendDeclared(b, page, now, tag); + AppendConnectScreen(b, page, tag); + AppendChanges(b, page, tag); return b.ToString(); } @@ -87,28 +127,47 @@ public static string Render( /// the same content the graphical page hides behind "read as text". The three states of spec /// §5.4 are three different words here and never share one. /// - private static void AppendActivity(StringBuilder b, IReadOnlyList cells) + private static void AppendActivity(StringBuilder b, IReadOnlyList cells, string tag) { if (cells.Count == 0) { return; } - Heading(b, "When people are on (UTC)"); - Wrap(b, ActivitySummary.Sentence(cells)); + Heading(b, Say(tag, "activity.plain.heading")); + + // The same threshold the graphical page draws on, and the same words. Below it there is no + // grid there and no seven lines here: a week of prose about two measured hours would be this + // surface describing a shape the measurements do not have. + if (ActivitySummary.MeasuredDays(cells) < ActivitySummary.MeasuredDaysForGrid) + { + Wrap(b, ActivitySummary.Sparse(tag, cells)); + return; + } + + Wrap(b, ActivitySummary.Sentence(tag, cells)); b.AppendLine(); - foreach (var line in ActivitySummary.PerDay(cells)) + foreach (var line in ActivitySummary.PerDay(tag, cells)) { Wrap(b, line, " "); } b.AppendLine(); - b.AppendLine(" counted = we got in and read a number, including a measured zero"); - b.AppendLine(" uncounted = we got in and no number could be read"); - b.AppendLine(" no data = we have no measurement for that hour"); + + // The key. The three words on the left are the site's own — two of them the glossary's + // locked ids — rather than a third spelling invented for this surface: "no data" said here + // what "not measured" says everywhere else, which left a reader deciding whether the two + // were one state. Wrapped rather than padded to a column, because a language whose word for + // "uncounted" is four syllables must not push the line past eighty. + Key(b, tag, "activity.key.counted", "activity.key.counted.meaning"); + Key(b, tag, "state.uncounted", "activity.key.uncounted.meaning"); + Key(b, tag, "state.notMeasured", "activity.key.notMeasured.meaning"); } + private static void Key(StringBuilder b, string tag, string word, string meaning) => + Wrap(b, $"{Say(tag, word)} = {Say(tag, meaning)}", " "); + /// /// The trend in words: the direction first, then a line per week. /// @@ -124,15 +183,16 @@ private static void AppendActivity(StringBuilder b, IReadOnlyList /// function — which is the decoration §9 is testing for. /// /// - private static void AppendTrend(StringBuilder b, TrendSeries? trend) + private static void AppendTrend(StringBuilder b, TrendSeries? trend, string tag) { if (trend is null || trend.Days.Count == 0) { return; } - Heading(b, $"HOW MANY, OVER TIME ({trend.From:d MMM yyyy} – {trend.To:d MMM yyyy}, UTC)"); - Wrap(b, trend.Sentence); + Heading(b, $"{Say(tag, "trend.plain.heading")} " + + $"({Say(tag, "trend.plain.range", ("from", trend.From), ("to", trend.To))})"); + Wrap(b, trend.Sentence(tag)); if (!trend.HasAnyCount) { @@ -141,7 +201,7 @@ private static void AppendTrend(StringBuilder b, TrendSeries? trend) b.AppendLine(); - foreach (var line in trend.PerWeek()) + foreach (var line in trend.PerWeek(tag)) { Wrap(b, line, " "); } @@ -150,36 +210,42 @@ private static void AppendTrend(StringBuilder b, TrendSeries? trend) var range = new TrendRange(trend.From, trend.To); - b.AppendLine($" earlier: ?{range.Previous().Query}&plain=1"); - b.AppendLine(" a week is summarised over the days in it we counted; a week with none says so"); + b.AppendLine($" {Say(tag, "trend.plain.earlier")}: ?{range.Previous().Query}&plain=1"); + Wrap(b, Say(tag, "trend.plain.note"), " "); } /// The 90-day strip in words: the summary, then every spell that was not reachable. - private static void AppendReachable(StringBuilder b, ReachSummary? reach) + private static void AppendReachable(StringBuilder b, ReachSummary? reach, string tag) { if (reach is null) { return; } - Heading(b, $"Reachable (last {reach.Window} days)"); - Wrap(b, reach.Sentence); + Heading(b, $"{Say(tag, "reach.plain.heading")} " + + $"({Say(tag, "reach.plain.window", ("days", reach.Window))})"); + Wrap(b, reach.Sentence(tag)); - if (reach.Spells.Count == 0) + var spells = reach.Spells(tag); + if (spells.Count == 0) { return; } b.AppendLine(); - foreach (var spell in reach.Spells) + foreach (var spell in spells) { b.AppendLine($" {spell}"); } } - private static void AppendCapabilities(StringBuilder b, GamePage page) + private static void AppendCapabilities(StringBuilder b, GamePage page, string tag) { - Heading(b, $"Capabilities ({page.DisagreementCount} of {page.Capabilities.Count} disagree)"); + Heading(b, Say( + tag, + "game.plain.capabilities", + ("disagreeing", page.DisagreementCount), + ("total", page.Capabilities.Count))); // The same order the matrix uses: disagreements first, then measured-present, then absent, // then unknown. Two surfaces of one fact must not put it in two places. @@ -193,23 +259,24 @@ private static void AppendCapabilities(StringBuilder b, GamePage page) }) .ThenBy(c => c.Protocol, StringComparer.Ordinal)) { - var flag = c.Disagrees ? " ** disagree" : string.Empty; - b.AppendLine($" {c.Protocol,-10} measured: {Word(c.Measured),-7} declared: {Word(c.Declared)}{flag}"); + var flag = c.Disagrees ? " " + Say(tag, "game.plain.disagree") : string.Empty; + b.AppendLine($" {c.Protocol,-10} {Say(tag, "game.plain.measured")}: {Word(c.Measured),-7} " + + $"{Say(tag, "game.plain.declared.column")}: {Word(c.Declared)}{flag}"); } } - private static void AppendDeclared(StringBuilder b, GamePage page, DateTimeOffset now) + private static void AppendDeclared(StringBuilder b, GamePage page, DateTimeOffset now, string tag) { if (page.Declared.Count == 0) { return; } - Heading(b, "Declared by the game"); + Heading(b, Say(tag, "game.plain.declared")); foreach (var (name, chip) in page.Declared) { - b.AppendLine($" {name,-10} {chip.Value} {Label(chip, now)}"); + b.AppendLine($" {name,-10} {chip.Value} {Label(tag, chip, now)}"); } } @@ -230,48 +297,49 @@ private static void AppendDeclared(StringBuilder b, GamePage page, DateTimeOffse /// break, and a private spelling is one the fifth surface cannot reach even to obey. /// /// - internal static string Label(ProvenanceChip? chip, DateTimeOffset now) => chip is null + internal static string Label(string tag, ProvenanceChip? chip, DateTimeOffset now) => chip is null ? string.Empty - : $"({Provenance.How(chip)}, {Relative.Format(now - chip.LastConfirmedAt)}" - + (chip.IsStale ? ", stale)" : ")"); + : Messages.For( + tag, + chip.IsStale ? "chip.plain.stale" : "chip.plain", + new Dictionary(StringComparer.Ordinal) + { + ["how"] = Provenance.How(tag, chip), + ["age"] = Relative.Format(tag, now - chip.LastConfirmedAt), + }); /// - /// The connect screen with its SGR stripped. Colour codes are never announced, and the three - /// cases the frame has — suppressed, too small, oversized — are stated rather than left as an - /// absence for the reader to interpret. + /// The connect screen with its SGR stripped. Colour codes are never announced, and the cases the + /// frame has — suppressed, absent, too small — are stated rather than left as an absence for the + /// reader to interpret. /// - private static void AppendConnectScreen(StringBuilder b, GamePage page) + private static void AppendConnectScreen(StringBuilder b, GamePage page, string tag) { var screen = Ansi.Parse(page.ConnectScreen, page.ConnectScreenSuppressed); - Heading(b, "Connect screen"); + Heading(b, Say(tag, "game.plain.connectScreen")); switch (screen.State) { case AnsiScreenState.Suppressed: - b.AppendLine(" The owner asked us not to republish this game's connect screen."); + Wrap(b, Say(tag, "ansi.suppressed"), " "); return; case AnsiScreenState.Absent: - b.AppendLine(" No connect screen has been captured from this game."); + Wrap(b, Say(tag, "ansi.absent"), " "); return; case AnsiScreenState.TooSmall: - b.AppendLine($" Only {screen.RowCount} row(s) came back — too little to show."); + Wrap(b, Say(tag, "ansi.tooSmall", ("count", screen.RowCount)), " "); return; } // The same caption the figure carries, for the same reason: on a game whose bytes were not // UTF-8 this is how a reader learns which encoding they are looking at rather than blaming // their terminal. The two surfaces must not disagree about the screen. - var charset = page.ConnectScreenCharset is { Length: > 0 } read ? $", read as {read}" : string.Empty; - - b.AppendLine($" [connect screen: {screen.RowCount} lines, text only{charset}]"); - if (screen.IsOversized) - { - b.AppendLine($" Unusually long; the graphical page shows the first {Ansi.CropRows}."); - } - + b.AppendLine(" [" + (page.ConnectScreenCharset is { Length: > 0 } read + ? Say(tag, "ansi.plain.rows.readAs", ("count", screen.RowCount), ("charset", read)) + : Say(tag, "ansi.plain.rows", ("count", screen.RowCount))) + "]"); b.AppendLine(); foreach (var row in screen.Rows) { @@ -279,14 +347,14 @@ private static void AppendConnectScreen(StringBuilder b, GamePage page) } } - private static void AppendChanges(StringBuilder b, GamePage page) + private static void AppendChanges(StringBuilder b, GamePage page, string tag) { if (page.Changes.Count == 0) { return; } - Heading(b, "What changed"); + Heading(b, Say(tag, "game.plain.whatChanged")); foreach (var change in page.Changes.OrderByDescending(c => c.At)) { b.AppendLine($" {change.At:yyyy-MM-dd} {change.Summary}"); @@ -302,7 +370,8 @@ private static void AppendChanges(StringBuilder b, GamePage page) /// URL. A panel that only worked as a widget would fail §9's own test of itself: if a fact /// cannot survive in plain text, its graphic on the main site is decoration. /// - public static string RenderListing(GameListing listing, GameFilter filter, DateTimeOffset now) + public static string RenderListing( + GameListing listing, GameFilter filter, DateTimeOffset now, string tag = Locales.SourceTag) { ArgumentNullException.ThrowIfNull(listing); ArgumentNullException.ThrowIfNull(filter); @@ -322,7 +391,7 @@ public static string RenderListing(GameListing listing, GameFilter filter, DateT // The order, stated. A sorted list that does not say what it is sorted by is one a reader has // to reverse-engineer from the first few rows — and that is exactly how a tail of games // showing no number gets read as a tail of games with no players. - b.AppendLine($"Sorted by {FacetWords.Sort(filter.Sort)}"); + b.AppendLine($"Sorted by {FacetWords.Sort(tag, filter.Sort)}"); // And every order it could have been in, wrapped rather than run on: a text browser cannot // operate a @foreach (var choice in ReaderTheme.Choices) { - + } @@ -28,6 +35,8 @@ private ThemeChoice InForce => ReaderTheme.Of(HttpContext); + private string Tag => HttpContext.LocaleOf().Tag; + /// /// The page this control was rendered on, so choosing does not lose the reader's place. /// diff --git a/src/MUI.Web/Components/TrendGeometry.cs b/src/MUI.Web/Components/TrendGeometry.cs index 9b49eaa..eb898db 100644 --- a/src/MUI.Web/Components/TrendGeometry.cs +++ b/src/MUI.Web/Components/TrendGeometry.cs @@ -1,6 +1,8 @@ using System.Globalization; using System.Text; +using MUI.Web.Localization; + namespace MUI.Web.Components; /// One column of the trend chart — a day, and the strip of canvas it owns. @@ -188,7 +190,7 @@ public static IReadOnlyList BandPaths(TrendSeries series) } /// Counted days with no counted neighbour, which no path can carry. - public static IReadOnlyList Dots(TrendSeries series) + public static IReadOnlyList Dots(string tag, TrendSeries series) { var ceiling = Ceiling(series); @@ -197,7 +199,7 @@ public static IReadOnlyList Dots(TrendSeries series) .Select(run => new TrendDot( X(run[0]), Y(run[0].Day.Average ?? 0, ceiling), - run[0].Day.Label)) + run[0].Day.Label(tag))) .ToList(); } @@ -207,10 +209,10 @@ public static IReadOnlyList Dots(TrendSeries series) /// measured zero — which is the collapse of §5.4's middle state into a filled cell, and the /// worst bug this codebase can ship. It sits in its own gutter and the legend names it. /// - public static IReadOnlyList Ticks(TrendSeries series) => + public static IReadOnlyList Ticks(string tag, TrendSeries series) => Columns(series) .Where(c => c.Day.IsUncountable) - .Select(c => new TrendTick(c.X, c.Width, c.Day.Label)) + .Select(c => new TrendTick(c.X, c.Width, c.Day.Label(tag))) .ToList(); /// @@ -223,7 +225,7 @@ public static IReadOnlyList Ticks(TrendSeries series) => /// sixty month names into a grey smear. The exact ends of the range are printed as words above /// the chart, so thinning here loses nothing. /// - public static IReadOnlyList Months(TrendSeries series) + public static IReadOnlyList Months(string tag, TrendSeries series) { var apart = Width / 9; var months = new List(); @@ -240,7 +242,7 @@ public static IReadOnlyList Months(TrendSeries series) continue; } - months.Add(new TrendMonth(column.X, column.X / Width, Month(column.Day.Date))); + months.Add(new TrendMonth(column.X, column.X / Width, Month(tag, column.Day.Date))); } return months; @@ -357,8 +359,21 @@ private static string Path(IEnumerable<(double X, double Y)> points) return b.ToString().TrimEnd(); } - private static string Month(DateOnly date) => - date.Month == 1 ? date.ToString("MMM yyyy") : date.ToString("MMM"); + /// + /// A rule's label: the month, and the year as well where the calendar turns over. + /// + /// + /// Through the message pipeline rather than ToString("MMM"), which reads the month name + /// off whatever culture the thread happens to be under — the request's, in one place, and the + /// invariant one in the headless renderer and the tests. The pattern carries the date style, so + /// a locale that writes the year first says so in its own copy instead of being given an + /// English ordering with translated words in it. + /// + private static string Month(string tag, DateOnly date) => + Messages.For( + tag, + date.Month == 1 ? "trend.axis.monthYear" : "trend.axis.month", + new Dictionary(StringComparer.Ordinal) { ["d"] = date }); private static double Y(double value, int ceiling) => Baseline - (value / ceiling * (Baseline - TopPad)); diff --git a/src/MUI.Web/Components/TrendSeries.cs b/src/MUI.Web/Components/TrendSeries.cs index d9bdaf8..3ff864e 100644 --- a/src/MUI.Web/Components/TrendSeries.cs +++ b/src/MUI.Web/Components/TrendSeries.cs @@ -1,6 +1,7 @@ using System.Globalization; using MUI.Catalog; +using MUI.Web.Localization; namespace MUI.Web.Components; @@ -45,16 +46,49 @@ public sealed record TrendDay( /// public int? Typical => Mean is { } m ? (int)Math.Floor(m) : null; - public string Label => this switch + /// + /// What one column says, as a sentence in the reader's language. + /// + /// + /// + /// Four shapes and the reader gets exactly one, so all four are ids and none is assembled. The + /// English this replaces glued a date to a fragment and a number to the word "probes", which is + /// two facts about English — the month name and the plural rule — standing where a measurement + /// was supposed to be. Ninety of these are drawn per game, which is why this one method was most + /// of what the site still said in English however it was asked. + /// + /// + /// The gap and the uncountable day are two different ids and must stay two. A day nobody + /// measured names no cause — a failed probe writes no presence row at all, so it covers a dial + /// we could not complete and a day we never dialled alike — and a day probed all through without + /// a readable count is a measurement of a game that was answering. Collapsing them is §5.4's + /// worst bug, and it is a collapse a translator could make silently, so the two ids read nothing + /// like each other and a test holds them apart in every locale. + /// + /// + public string Label(string tag) => this switch { - { IsGap: true } => $"{Date:d MMM yyyy} — no measurement", - { IsUncountable: true } => $"{Date:d MMM yyyy} — probed, no count could be read", - { Min: { } lo, Max: { } hi } when lo == hi => - $"{Date:d MMM yyyy} — {lo} players, every one of {Probes(CountedSamples)}", - _ => $"{Date:d MMM yyyy} — {Typical} on average, {Min}–{Max} across {Probes(CountedSamples)}", + { IsGap: true } => Messages.Say(tag, "trend.day.notMeasured", ("d", Date)), + { IsUncountable: true } => Messages.Say(tag, "trend.day.notCounted", ("d", Date)), + + // Every probe returning the same number is its own sentence rather than a range with that + // number written twice: "24–24" is arithmetic printed where a measurement was asked for. + { Min: { } lo, Max: { } hi } when lo == hi => Messages.Say( + tag, + "trend.day.flat", + ("d", Date), + ("count", lo), + ("probes", CountedSamples)), + + _ => Messages.Say( + tag, + "trend.day.counted", + ("d", Date), + ("typical", Typical), + ("low", Min), + ("high", Max), + ("probes", CountedSamples)), }; - - private static string Probes(int n) => n == 1 ? "1 probe" : $"{n} probes"; } /// @@ -113,26 +147,32 @@ public sealed record TrendSeries(DateOnly From, DateOnly To, IReadOnlyList - public string Sentence + public string Sentence(string tag) { - get + if (!HasAnyCount) { - if (!HasAnyCount) - { - return HasAnyMeasurement - ? "Probed in this range, and no player count could be read from any of it." - : "No measurement in this range."; - } + // Neither of these names a cause, and the two are different facts: probed and never + // countable is a measurement of a game that answered, and no measurement is a statement + // about our crawl and not about their game. + return Messages.For(tag, HasAnyMeasurement + ? "trend.none.probed" + : "trend.none.notMeasured"); + } - var counted = Days.Where(d => d.IsCounted).ToList(); - var typical = Median(counted.Select(d => d.Average!.Value)); - var peak = Ceiling; + var counted = Days.Where(d => d.IsCounted).ToList(); + var typical = Median(counted.Select(d => d.Average!.Value)); - var start = $"Typically {Number(typical)} on, peaking at {peak}, " - + $"over {CountedDays} of {Measured(Days.Count)}."; + var start = Messages.Say( + tag, + "trend.summary", + ("typical", Number(typical)), + ("peak", Ceiling), + ("counted", CountedDays), + ("days", Days.Count)); - return Direction(counted) is { } direction ? $"{start} {direction}" : start; - } + // Two sentences and not one with a word substituted in: "Steady across the range" is a whole + // clause, and a language that opens with the direction has nowhere to put it otherwise. + return Direction(tag, counted) is { } direction ? $"{start} {direction}" : start; } /// One line per week, which is the "read as text" disclosure and the plain rendering. @@ -148,13 +188,13 @@ public string Sentence /// with nothing counted still says which kind of nothing it was. /// /// - public IEnumerable PerWeek() + public IEnumerable PerWeek(string tag) { foreach (var week in Days.Chunk(7)) { var span = week.Length == 1 - ? $"{week[0].Date:d MMM}" - : $"{week[0].Date:d MMM}–{week[^1].Date:d MMM}"; + ? Messages.Say(tag, "trend.week.oneDay", ("d", week[0].Date)) + : Messages.Say(tag, "trend.week.span", ("from", week[0].Date), ("to", week[^1].Date)); var counted = week.Where(d => d.IsCounted).ToList(); var uncountable = week.Count(d => d.IsUncountable); @@ -162,31 +202,41 @@ public IEnumerable PerWeek() if (counted.Count == 0) { - yield return uncountable > 0 - ? $"{span}: probed, no count could be read" - : $"{span}: not measured"; + yield return Messages.Say( + tag, + uncountable > 0 ? "trend.week.notCounted" : "trend.week.notMeasured", + ("span", span)); continue; } - var typical = Number(Median(counted.Select(d => d.Average!.Value))); - var high = counted.Max(d => d.Max!.Value); - var line = $"{span}: typically {typical}, peak {high}, {Measured(counted.Count)} counted"; + var line = Messages.Say( + tag, + "trend.week.counted", + ("span", span), + ("typical", Number(Median(counted.Select(d => d.Average!.Value)))), + ("peak", counted.Max(d => d.Max!.Value)), + ("days", counted.Count)); + // Folded through a two-argument message rather than joined on a comma written here: the + // separator belongs to a language, and Chinese uses neither of ours. if (uncountable > 0) { - line += $", {Measured(uncountable)} probed without a count"; + line = Join(tag, line, Messages.Say(tag, "trend.week.uncounted", ("count", uncountable))); } if (unmeasured > 0) { - line += $", {Measured(unmeasured)} not measured"; + line = Join(tag, line, Messages.Say(tag, "trend.week.unmeasured", ("count", unmeasured))); } yield return line; } } + private static string Join(string tag, string line, string clause) => + Messages.Say(tag, "trend.week.and", ("line", line), ("clause", clause)); + /// /// The rollup's buckets as a day per column, with the days it does not hold left as gaps. /// @@ -213,7 +263,7 @@ public static TrendSeries Over(DateOnly from, DateOnly to, IReadOnlyList counted) + private static string? Direction(string tag, IReadOnlyList counted) { if (counted.Count < 6) { @@ -236,16 +286,21 @@ public static TrendSeries Over(DateOnly from, DateOnly to, IReadOnlyList before - ? $"Up about {change}% from the start of the range to the end." - : $"Down about {change}% from the start of the range to the end."; + return Messages.Say( + tag, + after > before ? "trend.direction.up" : "trend.direction.down", + ("change", change)); } + private static double Median(IEnumerable values) { var sorted = values.Order().ToList(); @@ -256,6 +311,4 @@ private static double Median(IEnumerable values) /// A count of players, floored — see for why down. private static string Number(double value) => Math.Floor(value).ToString("0", CultureInfo.InvariantCulture); - - private static string Measured(int n) => n == 1 ? "1 day" : $"{n} days"; } diff --git a/src/MUI.Web/Components/TrendShape.cs b/src/MUI.Web/Components/TrendShape.cs index a71ea8f..19805ce 100644 --- a/src/MUI.Web/Components/TrendShape.cs +++ b/src/MUI.Web/Components/TrendShape.cs @@ -1,3 +1,5 @@ +using MUI.Web.Localization; + namespace MUI.Web.Components; /// How the trend is drawn. @@ -41,6 +43,14 @@ public static TrendShape Parse(string? value) => /// What the address calls it. public static string Slug(this TrendShape shape) => shape is TrendShape.Bar ? "bar" : "line"; - /// What the selector calls it. - public static string Label(this TrendShape shape) => shape is TrendShape.Bar ? "bars" : "line"; + /// + /// What the selector calls it, in the reader's language. + /// + /// + /// Separate from , which is what the address calls it and stays + /// English for ever: a URL a reader copies out of a German page has to keep working when it is + /// pasted into an English one, so the query value is machine voice and only the label is not. + /// + public static string Label(this TrendShape shape, string tag) => + Messages.For(tag, shape is TrendShape.Bar ? "trend.shape.bar" : "trend.shape.line"); } diff --git a/src/MUI.Web/Components/_Imports.razor b/src/MUI.Web/Components/_Imports.razor index cc1b407..dba9550 100644 --- a/src/MUI.Web/Components/_Imports.razor +++ b/src/MUI.Web/Components/_Imports.razor @@ -6,3 +6,6 @@ @using MUI.Web.Components @using MUI.Web.Components.Layout @using MUI.Web.Data +@* Every component writes a link and most already say a sentence, and both go through here: a page + that forgot the using wrote its addresses in English on a page in another language. *@ +@using MUI.Web.Localization diff --git a/src/MUI.Web/CrawlerContact.cs b/src/MUI.Web/CrawlerContact.cs index 6af4deb..15ac98e 100644 --- a/src/MUI.Web/CrawlerContact.cs +++ b/src/MUI.Web/CrawlerContact.cs @@ -1,3 +1,5 @@ +using MUI.Web.Localization; + namespace MUI.Web; /// @@ -39,11 +41,15 @@ public static IEndpointRouteBuilder MapMuiCrawlerContact(this IEndpointRouteBuil // HEAD as well as GET: a link checker asking whether the address we published still works is // exactly the reader this route is for, and MapGet alone answers one with 405. - endpoints.MapMethods(Path, [HttpMethods.Get, HttpMethods.Head], (HttpRequest request) => + endpoints.MapMethods(Path, [HttpMethods.Get, HttpMethods.Head], (HttpContext context) => // The query string travels with it, and the fragment goes last because that is where a // fragment goes: ?plain=1 is a real second surface (§9), and an admin who asked for the - // plain rendering of the page that explains us should get it. - TypedResults.Redirect($"/about{request.QueryString}#{Fragment}")); + // plain rendering of the page that explains us should get it. And the locale travels + // with it too, for a reader who already had one: this lands on a page, and a page is a + // document in a language. + TypedResults.Redirect(LocaleRouting.Link( + context.LocaleOf().Tag, + $"/about{context.Request.QueryString}#{Fragment}"))); return endpoints; } diff --git a/src/MUI.Web/Fixtures/FixtureGameQueries.cs b/src/MUI.Web/Fixtures/FixtureGameQueries.cs index defa936..8f06e62 100644 --- a/src/MUI.Web/Fixtures/FixtureGameQueries.cs +++ b/src/MUI.Web/Fixtures/FixtureGameQueries.cs @@ -19,6 +19,15 @@ namespace MUI.Web.Fixtures; /// states, and those states are the product. /// /// +/// Eldertale and Midnight Sun II are the pair the whole catalogue turns on, and Hollow Bell is +/// the third of them. Both of the first two show a listing row with no number above it, and one is a +/// game we counted and found empty while the other is a game we got into and could not read; Hollow +/// Bell is a game we could not get into at all. Three rows that look alike and three different facts, +/// which is what the uncounted and unreachable switches exist to keep apart — and a +/// fixture holding only one of the three could not tell whether they had been implemented or merely +/// spelled. +/// +/// /// The archived and suppressed entries are the design handoff's own exemplars rather than real /// games, deliberately: asserting that a named game is dead, or that its owner asked us to stop /// republishing it, is exactly the kind of claim this site may not make without a measurement. @@ -79,6 +88,30 @@ public sealed class FixtureGameQueries : IGameQueries, IAvailabilityHistory IsClaimed: true, PlayersNow: 9, Codebase: "Evennia", MeasuredProtocols: ["MSSP", "GMCP", "TLS"], LastReachableAt: Now.AddMinutes(-9)); + /// + /// Stopped answering six weeks ago and has not been archived: unreachable, and not uncounted. + /// + /// + /// + /// The state between "went dark" and "archived", which nothing else here occupies — every other + /// silent game in this fixture is already archived, so the whole of the demo's + /// unreachable facet sat behind the archive switch and the dark activity band was + /// a rung no game stood on. + /// + /// + /// It is the counter-example the pair of switches needs. Midnight Sun answers and cannot + /// be counted; this one cannot be reached at all. Both listing rows show no number and the + /// reasons are opposite, which is exactly the distinction a reader is being offered — and its + /// grid is empty rather than hatched, because we did not get in to fail to count. + /// + /// + private static readonly GameSummary HollowBell = new( + Guid.Parse("aaaaaaaa-0000-0000-0000-00000000000a"), "hollow-bell", "Hollow Bell", + "Has not answered since June. Still probed, and one answer puts it back.", + LifecycleState.Dark, IsClaimed: false, + PlayersNow: null, Codebase: "PennMUSH 1.8.5", MeasuredProtocols: ["MSSP"], + LastReachableAt: Now.AddDays(-44)); + private static readonly GameSummary Gaslight = new( Guid.Parse("aaaaaaaa-0000-0000-0000-000000000005"), "gaslight-row", "Gaslight Row", "Ceased answering in March 2023. We still try the door every week.", @@ -108,7 +141,8 @@ public sealed class FixtureGameQueries : IGameQueries, IAvailabilityHistory [ .. new[] { - Mush, Eldertale, Aardwolf, MidnightSun, Enormous, Ashen, Gaslight, Verdigris, Cinder, + Mush, Eldertale, Aardwolf, MidnightSun, Enormous, Ashen, HollowBell, Gaslight, + Verdigris, Cinder, } .Select(Labelled), ]; @@ -258,13 +292,47 @@ public async Task> ListAsync( // Through the same predicate the database reads, rather than a slug test of its own — the // two implementations disagreeing about which games a filter returns is the failure that // put the filtering itself into one shared function. - IsAdult: AdultContent.Declared(Genre(game), AdultMaterial(game))); + IsAdult: AdultContent.Declared(Genre(game), AdultMaterial(game)), + Uncounted: Uncounted(game), + + // The same rule and the same constant the Postgres reader applies, for the same reason the + // line above is derived rather than declared. + Unreachable: FacetedSearch.NotReachedRecently(game.LastReachableAt, Now)); + + /// + /// Whether every hour this game answered in produced no number. + /// + /// + /// + /// Read off the fixture's own grid rather than listed beside it, so the panel cannot + /// promise something the heatmap on the game's page contradicts — the same argument + /// is derived by. A slug switch here would let the demo advertise + /// "uncounted" over a week of filled cells. + /// + /// + /// Both clauses matter, and the fixture exercises both. Eldertale is a measured zero in + /// every hour: counted, and emphatically not this. Hollow Bell answered in no hour at all: not + /// measured, which names no cause and is not this either. Midnight Sun answered in every hour and + /// produced a number in none, which is the one game here that is uncounted. Every other game has + /// one hatched hour beside its counts and is not uncounted for it. + /// + /// + private static bool Uncounted(GameSummary game) + { + var week = Activity(game); + + return week.Any(c => c.IsUnmeasurable) && !week.Any(c => c.IsCounted); + } private static ActivityBand Band(GameSummary g) => g.Slug switch { "gaslight-row" or "verdigris" => ActivityBand.Archived, "eldertale" => ActivityBand.ActiveThisWeek, "midnight-sun" => ActivityBand.Quiet, + + // Not reached in six weeks, and not archived — the one rung of this scale no fixture game + // stood on, so the demo panel never drew it. + "hollow-bell" => ActivityBand.Dark, _ => ActivityBand.PlayersNow, }; @@ -392,6 +460,7 @@ public Task> ForGameAsync( "midnight-sun" => [Endpoint("midnightsun2.org", 3000, "telnet", tls: false, sinceDays: 1500)], "batmud" => [Endpoint("bat.org", 23, "telnet", tls: false, sinceDays: 5000)], "ashen-court" => [Endpoint("ashen.example", 4000, "tls", tls: true, sinceDays: 600)], + "hollow-bell" => [Endpoint("hollowbell.example", 4201, "telnet", tls: false, sinceDays: 2400)], "gaslight-row" => [Endpoint("gaslight.example", 4201, "telnet", tls: false, sinceDays: 3000)], "verdigris" => [Endpoint("verdigris.example", 6250, "telnet", tls: false, sinceDays: 800)], _ => [Endpoint("eldertale.example", 4000, "telnet", tls: false, sinceDays: 1200)], @@ -583,6 +652,15 @@ AvailabilityInterval Span(double fromDaysAgo, double? toDaysAgo, AvailabilitySta Span(60, 58, AvailabilityState.Unreachable, FailureCause.Timeout), Span(58, null, AvailabilityState.Reachable, FailureCause.None), ], + + // Answered for years and then stopped, with the run still open. This is what the + // `unreachable` facet actually reads: an interval, which can say we tried and got + // nothing — where the empty half of a heatmap could not, and may not be asked to. + "hollow-bell" => + [ + Span(2400, 44, AvailabilityState.Reachable, FailureCause.None), + Span(44, null, AvailabilityState.Unreachable, FailureCause.Timeout), + ], _ => [ Span(4000, 51, AvailabilityState.Reachable, FailureCause.None), @@ -731,8 +809,9 @@ public Task FeedsAsync(CancellationToken cancellationToken = defa ], WentDark: [ - Event("verdigris", Now.AddDays(-6), - "unreachable since 24 July · connection refused · page stays, we keep knocking weekly"), + // The cause, and not the promise to keep probing: the section heading says that + // once, rather than every row saying it again. + Event("verdigris", Now.AddDays(-6), "connection refused"), ], CameBack: [ diff --git a/src/MUI.Web/Localization/Glossary.cs b/src/MUI.Web/Localization/Glossary.cs new file mode 100644 index 0000000..698282f --- /dev/null +++ b/src/MUI.Web/Localization/Glossary.cs @@ -0,0 +1,211 @@ +namespace MUI.Web.Localization; + +/// +/// The grammatical gender and number of the noun a provenance word describes. +/// +/// +/// Metadata for the translator, not a fact about English. "Measured" is one string in +/// English and four in Russian — измерен / измерена / измерено / измерены — chosen by the gender and +/// number of whatever it describes, and this site applies that one word to a count, a game, a value, +/// a capability and a connect screen. A single reused string is guaranteed to be wrong somewhere, +/// and a wrong-gender adjective does not read as a typo to a native speaker; it reads as illiterate, +/// which is an expensive impression for a site whose credibility is its product. +/// +public enum Subject +{ + /// The word modifies nothing — a bare column kicker. + Standalone, + + Masculine, + Feminine, + Neuter, + Plural, +} + +/// One locked string: what it must go on meaning, and what it may not become. +/// The context-keyed id. Never shared between two contexts. +/// The source text, which is also the fallback. +/// What the word agrees with, where it agrees with anything. +/// +/// Why this string is locked, shipped to translators as the brief rather than withheld. +/// +public sealed record LockedString(string Id, string English, Subject Subject, string Rationale); + +/// +/// The strings a translation may not paraphrase. +/// +/// +/// +/// Every row here exists because a plausible synonym in the target language would make the site +/// claim something it did not measure. That is the only kind of localization bug that damages +/// this product rather than merely embarrassing it. "not measured", "uncounted", +/// "reachable, count unreadable" and "unreachable" are four distinct states and the whole argument +/// of the site is that it never conflates them — and a translation engine treats them as stylistic +/// variants of *unavailable* and picks whichever target phrase is most frequent. In German all four +/// plausibly become "nicht verfügbar", and the reader can no longer tell a game that answered from +/// one that did not. +/// +/// +/// Ownership is maintainers only, and the rationale ships with the string. Contributors +/// translate everything outside this file freely. Inside it, a change is a maintainer action — and +/// the reason is published alongside each entry so a contributor can see why a string is locked +/// rather than reading the lock as distrust. A well-meaning "improvement" that arrives as a pull +/// request nobody knows how to refuse is the failure this is written to prevent. +/// +/// +/// Where a locale has no approved translation for a locked string, the English shows. A +/// reader who meets one English phrase inside a German sentence learns something true — that this +/// particular claim has not been translated yet. A smoothed-over mistranslation teaches them +/// something false, and they have no way to tell. +/// +/// +/// And never machine-translate these, even temporarily. A sighted reader who meets a garbled +/// label can look at the graphic and correct for it. A blind reader has only the label: a bad +/// translation of alt text or of a provenance word is the one case where a wrong string leaves +/// somebody with no way to recover, and it is precisely the case where English would have served +/// them better. +/// +/// +public static class Glossary +{ + /// Every locked id, with its English and the reason it is locked. + public static IReadOnlyList Locked { get; } = + [ + // ── provenance, per context ────────────────────────────────────────────────────────── + // One id per subject, because English collapses four Russian forms into one word. Never let + // two contexts share an id just because the source language cannot tell them apart. + new("provenance.count.measured", "measured", Subject.Feminine, + "We connected and read this number ourselves. The strongest claim on the site; must not " + + "weaken to 'verified' or 'confirmed', which imply a second party checked it."), + new("provenance.game.measured", "measured", Subject.Neuter, + "The same claim as provenance.count.measured, agreeing with the game rather than with the " + + "count. Separate id because English collapses the two and inflected languages do not."), + new("provenance.capability.measured", "measured", Subject.Plural, + "The same claim again, agreeing with a set of capabilities — plural, where the two above are " + + "singular. A shared id would be wrong here in every gendered language."), + new("provenance.screen.measured", "measured", Subject.Masculine, + "The same claim once more, agreeing with the connect screen. Four subjects, four ids: this is " + + "the entry that shows why the id space is keyed by context and not by word."), + new("kicker.measured", "measured", Subject.Standalone, + "A bare column header, modifying nothing — which is why it needs an id of its own rather " + + "than borrowing one that agrees with something."), + + new("provenance.count.declared", "declared", Subject.Feminine, + "The game said so and we did not check. Must not become a synonym of measured: the whole " + + "distinction is that this one is hearsay."), + new("provenance.game.declared", "declared", Subject.Neuter, + "The same hearsay claim as provenance.count.declared, agreeing with the game rather than with " + + "the count. Separate id for the same reason its measured counterpart has one."), + new("provenance.capability.declared", "declared", Subject.Plural, + "The same claim again, agreeing with a set of capabilities — plural, where the two above are " + + "singular. A shared id would be wrong here in every gendered language."), + new("kicker.declared", "declared", Subject.Standalone, + "The bare column header, modifying nothing — which in an inflected language is a different " + + "form from any of the three above, and so needs an id of its own."), + + new("provenance.derived", "derived", Subject.Neuter, + "We computed it from things we measured. Not observed directly, and not claimed by " + + "anybody — the third state exists so neither of the other two has to stretch."), + new("kicker.derived", "derived", Subject.Standalone, + "The bare column header, modifying nothing. Same argument as the two kickers above."), + + // ── the four kinds of absence, which are four different facts ──────────────────────── + new("state.notMeasured", "not measured", Subject.Standalone, + "We have not looked yet. Distinct from every kind of failure — must not become " + + "'unavailable', which implies we tried and something refused."), + new("state.uncounted", "uncounted", Subject.Standalone, + "The game answered and we could not read a count. Never zero, never empty. The string " + + "most likely to be mistranslated into 'no players', which is defamatory to a busy game."), + new("state.unreachable", "unreachable", Subject.Standalone, + "We tried and got nothing back. Must not imply the game is gone: the site keeps probing " + + "for ever and games come back."), + new("state.notCounted", "not counted", Subject.Standalone, + "The listing cell's own wording for an unreadable count. Never rendered as 0."), + + // ── the words the whole product rests on ───────────────────────────────────────────── + new("term.connected", "connected", Subject.Standalone, + "A count of open connections, not of people. Must not become 'players', 'users' or " + + "'online members' — one person with two clients is two connections and a bot is a " + + "connection. The grammatically safe phrasing is also the more truthful one."), + new("term.unclaimed", "unclaimed", Subject.Standalone, + "No owner has taken over the record. Administrative, and not a judgement about the " + + "game's quality or activity."), + new("term.claimedByOwner", "claimed by its owner", Subject.Standalone, + "Names the role, never the person, and assumes no gender for somebody real. The passive " + + "is the point rather than a shortening."), + new("term.stillProbed", "still probed", Subject.Standalone, + "We keep trying even though it is dark. Preserves the promise that a game which comes " + + "back will be noticed."), + new("term.typical", "typical", Subject.Standalone, + "A median over a stated window. A statistical term — must not collapse into 'average' " + + "with the one below it."), + new("term.peak", "peak", Subject.Standalone, + "The highest count seen in a stated window. Must not collapse into 'average'."), + + // ── the two accessibility promises ─────────────────────────────────────────────────── + new("a11y.readAsText", "read as text", Subject.Standalone, + "The label on every graphic's text alternative. Must read as an EQUIVALENT, not a " + + "summary or a fallback: a reader choosing it should expect nothing to be missing."), + new("a11y.plainText", "plain text", Subject.Standalone, + "The ?plain=1 mirror. Must not become 'simple' or 'basic', which suggest a reduced " + + "version — it is the complete page without the graphics."), + new("a11y.skipToContent", "skip to content", Subject.Standalone, + "The first thing a keyboard reader meets on every page."), + new("a11y.asciiBanner", "ASCII banner: the connect screen of {game}.", Subject.Standalone, + "The one-line alternative for a connect screen's artwork, and a blind reader's only " + + "route into that block. It names the drawing's kind and the game, and never describes " + + "the art: a paragraph about somebody else's ASCII would be our reading of their work " + + "presented as a fact about their game. Keep {game} exactly as it is."), + ]; + + private static readonly Dictionary ById = + Locked.ToDictionary(l => l.Id, StringComparer.Ordinal); + + /// Whether an id may only be changed by a maintainer. + public static bool IsLocked(string id) => ById.ContainsKey(id); + + /// The locked entry for an id, or null. + public static LockedString? Of(string id) => ById.GetValueOrDefault(id); + + /// + /// The brief a translator is handed, rather than a spreadsheet of bare strings. + /// + /// + /// Generated from the same data the site renders, so the document a translator works from cannot + /// drift from the strings that ship. The subject is stated because that is what an inflected + /// language keys agreement off, and it is invisible in the English. + /// + public static string Brief() + { + var b = new System.Text.StringBuilder(); + + b.AppendLine("# The locked glossary"); + b.AppendLine(); + b.AppendLine("Every string below is locked. Translate it, and preserve what the note says it"); + b.AppendLine("means — a plausible synonym here makes the site claim something it did not"); + b.AppendLine("measure, which is the only kind of localization bug that damages the product."); + b.AppendLine(); + b.AppendLine("Where you have no approved translation, leave it: the English will show, and a"); + b.AppendLine("reader who meets one English phrase learns something true."); + b.AppendLine(); + + foreach (var entry in Locked) + { + b.AppendLine($"## {entry.Id}"); + b.AppendLine(); + b.AppendLine($" {entry.English}"); + b.AppendLine(); + + if (entry.Subject is not Subject.Standalone) + { + b.AppendLine($"Agrees with: {entry.Subject.ToString().ToLowerInvariant()}."); + b.AppendLine(); + } + + b.AppendLine(entry.Rationale); + b.AppendLine(); + } + + return b.ToString(); + } +} diff --git a/src/MUI.Web/Localization/IcuMessage.cs b/src/MUI.Web/Localization/IcuMessage.cs new file mode 100644 index 0000000..85efd4d --- /dev/null +++ b/src/MUI.Web/Localization/IcuMessage.cs @@ -0,0 +1,374 @@ +using System.Collections.Concurrent; +using System.Globalization; +using System.Text; + +namespace MUI.Web.Localization; + +/// +/// ICU MessageFormat, rendered. +/// +/// +/// +/// What it is for. 545 on, 23 games and 7 days measured · 168 probes each +/// glue a number to an English fragment in English word order, and there is nowhere in that for a +/// translator to intervene without editing markup. One message per fact, the count as a named +/// argument and the plural clause written out in full is what gives them somewhere to stand — and +/// it is what stopped this site rendering "1 games" on every accessible name in its facet panel. +/// +/// +/// The whole of MessageFormat 1.0. Simple arguments, number, date and +/// time with styles and skeletons, plural and selectordinal with offset: +/// and =value matches, select, arbitrary nesting, and ICU's apostrophe quoting in its +/// default mode. See for the parse and for why choice is refused +/// and MessageFormat 2.0 is not yet the target. +/// +/// +/// Patterns are parsed once. A message is rendered on every request that draws the surface it +/// belongs to — the facet panel alone renders forty of them per page — and re-parsing a string that +/// has not changed since startup is work nobody asked for. The cache is keyed on the pattern text +/// because that is the only thing the parse depends on; the locale is applied at format time. +/// +/// +/// The cache is unbounded, and that is safe only because of what may be passed to it. A +/// pattern comes from a resource bundle or from a literal in this repository, so the set of distinct +/// keys is fixed when the process starts and is a few hundred entries. A pattern built from +/// request data must never reach or — a querystring +/// or a game's own name interpolated into a pattern would make this dictionary grow without limit +/// for as long as somebody kept asking. Interpolate into an argument, which is not cached +/// and not parsed; the pattern is the part that has to be written down in advance. +/// +/// +public static class IcuMessage +{ + private static readonly ConcurrentDictionary Parsed = new(StringComparer.Ordinal); + + private static readonly IReadOnlyDictionary NoArguments = + new Dictionary(StringComparer.Ordinal); + + /// Renders for . + /// The pattern is malformed, or an argument is missing. + public static string Format( + string pattern, + string tag, + IReadOnlyDictionary? arguments = null) + { + ArgumentNullException.ThrowIfNull(pattern); + ArgumentNullException.ThrowIfNull(tag); + + var b = new StringBuilder(pattern.Length); + + Render(Compile(pattern), tag, arguments ?? NoArguments, b, hash: null); + + return b.ToString(); + } + + /// The parsed form of a pattern, from the cache or freshly parsed. + public static MessagePattern Compile(string pattern) + { + ArgumentNullException.ThrowIfNull(pattern); + + return Parsed.GetOrAdd(pattern, MessagePattern.Parse); + } + + private static void Render( + MessagePattern message, + string tag, + IReadOnlyDictionary args, + StringBuilder b, + string? hash) + { + foreach (var part in message.Parts) + { + switch (part) + { + case LiteralPart literal: + b.Append(literal.Text); + break; + + case HashPart: + // Outside a plural branch the parser never produces one of these, so a `#` that + // reaches here always has a number behind it. + b.Append(hash); + break; + + case ArgumentPart argument: + Argument(argument, tag, args, b, hash); + break; + } + } + } + + private static void Argument( + ArgumentPart argument, + string tag, + IReadOnlyDictionary args, + StringBuilder b, + string? hash) + { + var culture = Culture(tag); + + if (!args.TryGetValue(argument.Name, out var value)) + { + throw new FormatException($"No argument named '{argument.Name}' was supplied."); + } + + switch (argument.Kind) + { + case ArgumentKind.None: + b.Append(Convert.ToString(value, culture)); + return; + + case ArgumentKind.Number: + b.Append(Number(argument.Name, value, argument.Style, culture)); + return; + + case ArgumentKind.Date: + case ArgumentKind.Time: + b.Append(Temporal(value, argument.Kind, argument.Style, culture)); + return; + + case ArgumentKind.Plural: + case ArgumentKind.SelectOrdinal: + Plural(argument, tag, args, b, value, culture); + return; + + case ArgumentKind.Select: + var chosen = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty; + + Render( + argument.Branches.GetValueOrDefault(chosen) ?? argument.Branches["other"], + tag, args, b, hash); + return; + } + } + + private static void Plural( + ArgumentPart argument, + string tag, + IReadOnlyDictionary args, + StringBuilder b, + object? value, + CultureInfo culture) + { + var number = Quantity(argument.Name, value); + var operands = value is PluralOperands given ? given : PluralOperands.Of(number); + var kind = argument.Kind is ArgumentKind.SelectOrdinal + ? PluralKind.Ordinal + : PluralKind.Cardinal; + + // An `=value` match is tested against the number as written, before the offset — ICU says + // so, and it is what lets "=0 {nobody}" work in a message that also subtracts one. + var exact = "=" + Plain(number, operands.V); + + if (argument.Branches.TryGetValue(exact, out var matched)) + { + Render(matched, tag, args, b, Hash(number, operands.V, culture)); + return; + } + + // The category, and `#`, are both taken from the offset-adjusted number. "{n, plural, + // offset:1 other {and # others}}" over three people is "and 2 others". + var adjusted = number - argument.Offset; + var keyword = PluralRules.Keyword( + PluralRules.Of(tag, PluralOperands.Of(adjusted, operands.V), kind)); + + var branch = argument.Branches.GetValueOrDefault(keyword) ?? argument.Branches["other"]; + + Render(branch, tag, args, b, Hash(adjusted, operands.V, culture)); + } + + /// The signed number a caller passed, or a refusal naming the argument. + private static decimal Quantity(string name, object? value) => value switch + { + int i => i, + long l => l, + short s => s, + byte by => by, + decimal d => d, + double db => (decimal)db, + float f => (decimal)f, + + // Absolute by construction: the operands CLDR states are, so a caller handing them over + // directly has already discarded whatever sign the quantity had. + PluralOperands o => o.N, + + null => throw new FormatException($"'{name}' is a plural argument and was null."), + _ => throw new FormatException( + $"'{name}' is a plural argument and must be a number, not {value.GetType().Name}."), + }; + + /// + /// The number a # stands for, written exactly as {n, number} would write it. + /// + /// + /// + /// One bundle may not print one quantity two ways. # was the raw integer digits + /// and {n, number} was the culture's grouped form, so a listing past a thousand said + /// "1234 games" in a sentence and "1,234" in the column beside it — and in German the separator + /// is a full stop, which no invariant rendering reaches at all. ICU replaces # with the + /// argument's own formatted number, so the default pattern here is the default pattern there. + /// + /// + /// The sign travels with it. CLDR takes the absolute value to choose a category and never to + /// display one, and "3 games" for minus three is a measurement stated backwards. + /// + /// + private static string Hash(decimal number, int visibleFractionDigits, CultureInfo culture) => + number.ToString( + visibleFractionDigits == 0 ? "#,##0" : "#,##0." + new string('0', visibleFractionDigits), + culture); + + /// The same number as an = key, which is matched and never shown to anybody. + /// + /// Plain and invariant on purpose: it is compared against what a message author typed between + /// the braces, and a translator writing =1000 should not have to know which separator + /// their locale would have inserted. + /// + private static string Plain(decimal number, int visibleFractionDigits) => + number.ToString( + visibleFractionDigits == 0 ? "0" : "0." + new string('0', visibleFractionDigits), + CultureInfo.InvariantCulture); + + /// + /// {n, number, style}, with ICU's named styles and its :: skeletons. + /// + /// + /// Nothing on this site calls it, and it is here anyway. Counts, versions and ages are + /// machine output and stay in Western digits in every locale — Arabic-Indic digits have no + /// tabular figures in most faces, so a localized count column loses the alignment that is the + /// only reason it is a column. What this covers is prose: a percentage inside a sentence is a + /// number a reader reads rather than scans, and that one localizes. + /// + private static string Number(string name, object? value, string? style, CultureInfo culture) + { + decimal number; + + try + { + number = Convert.ToDecimal(value, CultureInfo.InvariantCulture); + } + catch (Exception e) when (e is InvalidCastException or OverflowException or FormatException) + { + // The documented contract on Format is FormatException, and Convert raises two others + // this could not have said anything about. Neither of them names the argument, and a + // page rendering forty messages needs to be told which one it was. + throw new FormatException( + $"'{name}' is a number argument and must be a number, " + + $"not {value?.GetType().Name ?? "null"}.", e); + } + + if (style is { Length: > 0 } && style.StartsWith("::", StringComparison.Ordinal)) + { + return Skeleton(number, style[2..].Trim(), culture); + } + + return style switch + { + null or "" => number.ToString("#,##0.###", culture), + "integer" => Math.Round(number, MidpointRounding.ToEven).ToString("#,##0", culture), + // Built rather than "P0": .NET's percent pattern inserts a space before the sign in + // several cultures and ICU's does not, so the two disagree on en for no reason a + // message author could predict. + "percent" => (number * 100m).ToString("#,##0.###", culture) + culture.NumberFormat.PercentSymbol, + "currency" => number.ToString("C", culture), + + // Anything else is a .NET format string, which is what ICU does with an unrecognised + // style too: it hands it to the underlying number formatter. + _ => number.ToString(style, culture), + }; + } + + /// The handful of number skeletons that mean anything without a full ICU behind them. + private static string Skeleton(decimal number, string skeleton, CultureInfo culture) + { + var parts = skeleton.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var format = "#,##0.###"; + var percent = false; + + foreach (var token in parts) + { + if (token is "percent") + { + percent = true; + continue; + } + + // .00 / .0 / .### — the fraction-precision stem, which is the one anybody writes. + if (token.StartsWith('.')) + { + format = "#,##0" + token; + continue; + } + + if (token is "group-off") + { + format = format.Replace("#,##0", "0", StringComparison.Ordinal); + } + } + + var rendered = (percent ? number * 100m : number).ToString(format, culture); + + return percent ? rendered + culture.NumberFormat.PercentSymbol : rendered; + } + + /// + /// {d, date, style} and {d, time, style}. + /// + /// + /// Also uncalled, and also deliberate. Every date this site prints goes through + /// , which states UTC explicitly and uses an abbreviated month + /// name rather than a numeric one — 08/17 means two different days on two continents. + /// This exists so a message can carry a date without the formatter refusing the + /// pattern, which is the difference between supporting the grammar and supporting the half of + /// it we happen to use. + /// + private static string Temporal(object? value, ArgumentKind kind, string? style, CultureInfo culture) + { + var when = value switch + { + DateTimeOffset offset => offset, + + // An Unspecified kind takes the machine's own offset from this constructor, so the same + // message rendered on two hosts said two different times and neither said which. This + // site is UTC everywhere on purpose — every date it prints says so — and a date that + // arrived without a zone did not arrive from somewhere else. + DateTime { Kind: DateTimeKind.Unspecified } bare => new DateTimeOffset(bare, TimeSpan.Zero), + DateTime dt => new DateTimeOffset(dt), + + // A day-grain fact carries a DateOnly, and every day-grain surface on this site — the + // trend chart's ninety columns, the reachability strip's ninety bars — has one to say. + // Refusing it would have meant each of those formatting its own date at the call site, + // which is the hard-coded month name this argument exists to remove. + DateOnly day => new DateTimeOffset(day.ToDateTime(TimeOnly.MinValue), TimeSpan.Zero), + _ => throw new FormatException($"A {kind} argument must be a date, not {value?.GetType().Name ?? "null"}."), + }; + + var date = kind is ArgumentKind.Date; + + if (style is { Length: > 0 } && style.StartsWith("::", StringComparison.Ordinal)) + { + // A date skeleton names the fields it wants; .NET has no skeleton engine, so the + // closest honest thing is the culture's own long or short pattern. + style = style.Contains('y', StringComparison.Ordinal) ? "long" : "short"; + } + + return style switch + { + null or "" or "medium" => when.ToString(date ? "d MMM yyyy" : "HH:mm:ss", culture), + "short" => when.ToString(date ? "d" : "t", culture), + "long" => when.ToString(date ? "D" : "T", culture), + "full" => when.ToString(date ? "D" : "T", culture), + _ => when.ToString(style, culture), + }; + } + + /// + /// The culture a tag names, or the invariant one where .NET has never heard of it. + /// + /// + /// 's cache rather than one of its own: the number formats a + /// message renders and the day names a sentence names are the same CLDR data, and two lookups + /// of one tag are two places for it to be answered differently. + /// + private static CultureInfo Culture(string tag) => Locales.CultureOf(tag); +} diff --git a/src/MUI.Web/Localization/LocaleRouting.cs b/src/MUI.Web/Localization/LocaleRouting.cs new file mode 100644 index 0000000..7e4ddef --- /dev/null +++ b/src/MUI.Web/Localization/LocaleRouting.cs @@ -0,0 +1,565 @@ +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace MUI.Web.Localization; + +/// Which locale this request is being answered in, and how it was decided. +/// The locale in force. +/// +/// Whether a path segment named it — which is what makes a locale linkable, cacheable and +/// indexable rather than a property of whoever's cookie jar the request arrived with. +/// +public sealed record LocaleContext(Locale Locale, bool FromPath) +{ + /// The tag, which is what every message lookup is keyed on. + public string Tag => Locale.Tag; + + /// The source locale, for a request nothing has decided yet. + public static LocaleContext Default { get; } = new(Locales.Source, FromPath: false); +} + +/// +/// The locale in the path, and the two ways a reader gets one. +/// +/// +/// +/// In the path and not in a header. A locale that lives only in a cookie or in +/// Accept-Language gives one URL two bodies: a shared link opens in the sender's language for +/// them and the recipient's for everybody else, a cache in front of the site serves whichever +/// arrived first to whoever asks next, and a search engine indexes one of them arbitrarily. +/// /de/games?plain=1 is one address for one document, which is the same argument the +/// querystring already wins for the filters. +/// +/// +/// The source locale has no prefix. /games is the canonical English address and +/// /en/games redirects to it, because two URLs for one document is the thing this is +/// avoiding — and every link written across this site, every bookmark and every inbound link +/// already spells the unprefixed one. +/// +/// +/// Accept-Language decides the first visit and nothing after it. A header is a +/// standing preference about content in general and not a choice about this site, so it is worth one +/// redirect and no more: once a reader has chosen, the cookie is what answers, and a reader who +/// deliberately opened the English page must not be bounced out of it by their browser's settings on +/// every request. +/// +/// +public static class LocaleRouting +{ + /// Where the middleware leaves its answer for the rest of the request. + public const string ItemKey = "mui.locale"; + + /// + /// Whether this request is being answered by a deployment somebody is reviewing. + /// + /// + /// + /// Asked of the request, because the alternatives were a global and an injection and both + /// had already failed. A component taking IWebHostEnvironment as a dependency cannot + /// be rendered without a web host behind it, and every headless component test in this suite + /// renders one without. A static bool written from composition fixed that and bought a + /// worse problem: a test process starts many hosts, in Development and in Production, so the + /// last one to start decided what the switcher listed on every request served by any of them. + /// + /// + /// The request's own services answer it, and a request that has none — a component rendered + /// with no HttpContext at all — is not a review build. Nothing has to be told; the + /// question simply has an answer wherever it is asked. + /// + /// + public static bool IsReviewBuild(this HttpContext? context) => + context?.RequestServices?.GetService()?.IsDevelopment() is true; + + /// The locale this request is being answered in. + public static LocaleContext LocaleOf(this HttpContext? context) => + context?.Items.TryGetValue(ItemKey, out var found) is true && found is LocaleContext ctx + ? ctx + : LocaleContext.Default; + + /// Reads the locale out of the path, or decides one for a request that carries none. + public static IApplicationBuilder UseMuiLocale(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + + return app.Use(async (context, next) => + { + var path = context.Request.Path.Value ?? "/"; + var segment = FirstSegment(path); + + // The same path, still escaped, for anything written into a Location header. + // + // Request.Path.Value is decoded: a segment containing %2F comes back as a bare slash and + // becomes two segments, a %23 comes back as a `#` and truncates the target at the + // fragment, a %3F becomes a `?` and turns the rest of the path into a query, and every + // non-ASCII character comes back raw into a header field that may not carry one. A + // reader following that redirect is sent somewhere they did not ask for. Splitting on + // '/' is still right here, because in the escaped form a literal slash is the only + // separator and an encoded one is three characters that are not it. + var escaped = context.Request.Path.ToUriComponent(); + + if (Locales.Find(segment) is { } named) + { + // The canonical English address carries no prefix, so /en/... is a second URL for a + // document that already has one. Permanent, because it always was one — but only for + // a request a redirect can carry. A 301 answering a POST is followed as a GET with + // the body dropped, so /en/theme would lose the theme it was posted. + if (named.Tag == Locales.SourceTag) + { + if (Redirectable(context.Request)) + { + context.Response.Redirect(Rest(escaped) + context.Request.QueryString, permanent: true); + return; + } + + context.Items[ItemKey] = LocaleContext.Default; + context.Request.Path = Rest(path); + + await next(context); + return; + } + + context.Items[ItemKey] = new LocaleContext(named, FromPath: true); + + // The rest of the pipeline routes, links and renders as though the prefix were not + // there — which is what lets every @page directive stay written once. + context.Request.PathBase = context.Request.PathBase.Add("/" + segment); + context.Request.Path = Rest(path); + + await next(context); + return; + } + + // No prefix, and nothing here may move this request. A locale is a property of a + // document, so only a request for a document is worth relocating. + if (!Redirectable(context.Request)) + { + context.Items[ItemKey] = Remembered(context) ?? LocaleContext.Default; + + await next(context); + return; + } + + // A reader who has chosen is sent to their choice; a reader who has not is offered one + // exactly once, off the header their browser sends. + var remembered = Locales.Find(context.Request.Cookies[Locales.CookieName]); + + if (remembered is { IsChoosable: true } && remembered.Tag != Locales.SourceTag) + { + context.Response.Redirect("/" + remembered.Tag + escaped + context.Request.QueryString); + return; + } + + if (remembered is null && Preferred(context.Request.Headers.AcceptLanguage) is { } guessed) + { + context.Response.Redirect("/" + guessed.Tag + escaped + context.Request.QueryString); + return; + } + + context.Items[ItemKey] = LocaleContext.Default; + + await next(context); + }); + } + + /// + /// Whether this request is one a locale redirect may move. + /// + /// + /// + /// The locale was a one-way door, and the switcher out of it was the thing it shut. A 302 + /// answering a POST is followed as a GET with the body discarded, and every control on this site + /// is a form: with mui_locale=de set, POST /theme was answered with a redirect to + /// /de/theme, which no endpoint serves, so a German reader could not change the theme — + /// and POST /locale went the same way, so they could not change the language back either. + /// + /// + /// The API and the crawler's own files are excluded for a different reason. They are not + /// documents in a language: /api/games answers the same JSON to every reader — it is + /// pinned to the source locale by name — and robots.txt and sitemap.xml have one + /// canonical address each, which is where a crawler looks and where the sitemap says they are. + /// Bouncing them through a prefix cost a round trip and published a second URL for a file that + /// is supposed to have exactly one. + /// + /// + private static bool Redirectable(HttpRequest request) => + (HttpMethods.IsGet(request.Method) || HttpMethods.IsHead(request.Method)) + && !IsUnlocalized(request.Path); + + /// The paths that are the same in every language, so a prefix says nothing about them. + public static bool IsUnlocalized(PathString path) => + path.StartsWithSegments(Api.ApiRoutes.Base, StringComparison.OrdinalIgnoreCase) + || IsFile(path); + + /// + /// The extensions this site serves as files rather than as documents. + /// + /// + /// + /// Named by extension because the stylesheet's address is not knowable. It carries a + /// content fingerprint — /app.gt0hup1p9v.css — which changes whenever the bytes do, so no + /// list of paths can hold it, and a reader with a locale cookie paid a redirect for the + /// stylesheet, the script and the touch icon on every page load. A 302 that answers a + /// request for a file is pure latency: the bytes are the same in every language. + /// + /// + /// An allowlist rather than "the last segment has a dot", because {Slug} is a route + /// parameter and a game's slug is not this file's to make promises about. A game called + /// foo.css would still be wrong, and wrong by one missing prefix on one page rather than + /// by a 404 — the request is answered in the reader's locale either way, because a request that + /// is not redirected still reads the cookie. + /// + /// + private static readonly string[] FileExtensions = + [ + ".css", ".js", ".map", ".json", ".xml", ".txt", ".webmanifest", + ".png", ".svg", ".ico", ".jpg", ".jpeg", ".gif", ".webp", ".avif", + ".woff", ".woff2", ".ttf", + ]; + + /// Whether this path names a file, which is the same file in every language. + private static bool IsFile(PathString path) + { + var value = path.Value; + + if (string.IsNullOrEmpty(value)) + { + return false; + } + + var lastSegment = value.AsSpan()[(value.LastIndexOf('/') + 1)..]; + var dot = lastSegment.LastIndexOf('.'); + + if (dot < 0) + { + return false; + } + + var extension = lastSegment[dot..]; + + foreach (var known in FileExtensions) + { + if (extension.Equals(known, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + /// The locale a reader has already chosen, where they have chosen one. + private static LocaleContext? Remembered(HttpContext context) => + Locales.Find(context.Request.Cookies[Locales.CookieName]) is { IsChoosable: true } chosen + ? new LocaleContext(chosen, FromPath: false) + : null; + + /// + /// The switcher's endpoint: remember a choice, and go back to the same page in it. + /// + /// + /// + /// Anti-forgery is off here, deliberately, and on the same reasoning as the theme control + /// beside it. Everything a forged post can achieve is that the victim's next page view is + /// in a language they did not pick — visible on arrival, stated in their own language's name in + /// the switcher, and undone by one click of the control that is already on the page. Nothing is + /// read, nothing is written that survives the reader clearing it, and this site holds no + /// user-specific state beyond a theme and a language. + /// + /// + /// The price of buying protection against that is not a token: it is an + /// <AntiforgeryToken /> in the header of every page, because that is where + /// the switcher is. The token rides in a cookie, so it would put a Set-Cookie on every + /// response from a site that otherwise sets none for a signed-out reader, and make every one of + /// those responses uncacheable by anything in front of us — and then answer a reader whose + /// cached page outlived its token with a 400 where they expected a language. + /// + /// + /// SameSite=Lax is not what makes this safe, and it is worth being exact about why. + /// SameSite governs when a cookie is sent, not whether one may be set: a + /// cross-site form post is a top-level navigation, the Set-Cookie in the answer is + /// stored, and a later top-level GET does carry a Lax cookie. The attack works. What makes it + /// not worth defending against is its consequence, which is that a stranger can change the + /// language of one page view. + /// + /// + /// The measure that would be worth having is a Sec-Fetch-Site check, which costs no + /// token, no cookie, no cache entry and no script. It belongs to this endpoint and the theme + /// endpoint together — one control guarded and its twin not is worse than neither — so it is a + /// change of its own rather than a line here. + /// + /// + public static IEndpointRouteBuilder MapMuiLocale(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + endpoints.MapPost(Locales.Path, async context => + { + var form = await context.Request.ReadFormAsync(); + var chosen = Locales.Find(form[Locales.Field]) ?? Locales.Source; + var back = Back(form[Locales.ReturnField]); + + var options = new CookieOptions + { + Path = "/", + HttpOnly = true, + SameSite = SameSiteMode.Lax, + + // Essential in the sense the consent rules mean: it is which language a reader + // asked to read in, held because they asked, and it identifies nobody. + IsEssential = true, + Secure = context.Request.IsHttps, + MaxAge = TimeSpan.FromDays(365), + }; + + if (chosen.Tag == Locales.SourceTag) + { + // The source language is the absence of a choice rather than a choice of its own, + // which is what stops a reader who picks English being pinned out of a future + // Accept-Language answer. Same shape as the theme control's "auto". + context.Response.Cookies.Delete(Locales.CookieName, options); + context.Response.Redirect(back); + return; + } + + context.Response.Cookies.Append(Locales.CookieName, chosen.Tag, options); + context.Response.Redirect(Link(chosen.Tag, back)); + }); + + return endpoints; + } + + /// + /// An address on this site, in the locale the page carrying it is being read in. + /// + /// + /// + /// Every internal link was written as an absolute path and emitted verbatim, so a German + /// page's links all pointed out of German. /de/games rendered href="/games", + /// href="/about", href="/find" — and a reader who followed a shared /de/… + /// link has no cookie, so nothing could send them back and their first click landed in English. + /// The locale is in the path precisely so it is linkable, shareable, cacheable and indexable; + /// a page that throws it away on every link has the property in its address and nowhere else. + /// + /// + /// Written once and called at every link site, rather than at each producer. The pages + /// build addresses in a dozen places — ListingLinks, FindScreen, the facet panel, + /// the reference library — and threading a locale through all of them would leave the next + /// producer to remember. This is the last thing that touches the string before it becomes an + /// attribute, so what it is given is whatever a caller had, and every shape it can be given is + /// answered here. + /// + /// + /// What is left exactly as it arrived, because none of it is an app path in a language: + /// anything not starting with / — a query-only ?plain=1 or ?window=30d, + /// which means this page, asked differently and is relative to it on purpose; a + /// fragment; an absolute URL; a mailto: — and anything starting // or /\, + /// which is another host wearing a path's clothes. names the rest: + /// the read API, robots.txt, sitemap.xml, the icons and the manifest each have one + /// canonical address, which is the same list the middleware refuses to redirect and not a second + /// copy of it. + /// + /// + /// The locale and not Request.PathBase, though the middleware puts the prefix + /// there and reading it back would have been cheaper. The two agree on every request that + /// arrives with a prefix and disagree on the one that does not: a request nothing may redirect — + /// a POST — is answered in the locale the reader's cookie names with an empty PathBase, + /// so a page rendered from it would link to English while reading German. The question a link + /// asks is which language is this page in, and that is what + /// answers. + /// + /// + public static string Link(string tag, string? path) + { + ArgumentNullException.ThrowIfNull(tag); + + if (string.IsNullOrEmpty(path) || tag == Locales.SourceTag || path[0] != '/') + { + return path ?? string.Empty; + } + + if (path.Length > 1 && path[1] is '/' or '\\') + { + return path; + } + + // The path alone decides, so /games?plain=1 is localized and /sitemap.xml?x=1 is not: the + // list is about which document an address names, and a querystring does not change that. + var cut = path.AsSpan().IndexOfAny('?', '#'); + + return IsUnlocalized(cut < 0 ? path : path[..cut]) ? path : "/" + tag + path; + } + + /// The same, for the request a component is being rendered inside. + /// + /// The form nearly every call site uses: a component already holds the HttpContext it + /// reads its messages from, so a link asks the same thing the words did. A component rendered + /// with no request at all — which is how this suite renders most of them — is in the source + /// locale, and there the answer is the path it was given. + /// + public static string Link(this HttpContext? context, string? path) => + Link(context.LocaleOf().Tag, path); + + /// + /// The same page, in every locale it exists in — for <link rel="alternate">. + /// + /// + /// x-default points at the unprefixed address, which is what tells a search engine that + /// the English URL is the one to show a reader whose language nothing here matches, rather than + /// having it pick one of the seven. + /// + public static IEnumerable<(string HrefLang, string Path)> Alternates(string pathWithinLocale) + { + ArgumentNullException.ThrowIfNull(pathWithinLocale); + + var path = pathWithinLocale.Length == 0 ? "/" : pathWithinLocale; + + yield return ("x-default", path); + + foreach (var locale in Locales.Offered) + { + // The same rule the links on the page follow, from the same function: an alternate that + // spelled the prefix itself would be a second copy of it to keep in step. + yield return (locale.Tag, Link(locale.Tag, path)); + } + } + + /// + /// The best offered locale for a browser's own list, or null to leave the reader where they are. + /// + /// + /// + /// Quality values are honoured because a browser sends them meaning something — de;q=0.9, + /// en;q=0.8 is a reader who reads both and prefers German — and a match on the language + /// subtag alone counts, so a reader asking for zh-CN reaches zh-Hans. Anything + /// scoring zero is a language the reader has explicitly refused. + /// + /// + /// English never wins here, because English is where the reader already is: returning it would + /// be a redirect to the page being served. + /// + /// + public static Locale? Preferred(string? acceptLanguage) + { + if (string.IsNullOrWhiteSpace(acceptLanguage)) + { + return null; + } + + var best = default(Locale); + var bestScore = 0d; + + foreach (var part in acceptLanguage.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + { + var bits = part.Split(';', StringSplitOptions.TrimEntries); + var tag = bits[0]; + + var quality = 1d; + + foreach (var parameter in bits.Skip(1)) + { + if (parameter.StartsWith("q=", StringComparison.OrdinalIgnoreCase) + && double.TryParse(parameter[2..], System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out var q)) + { + quality = q; + } + } + + if (quality <= 0 || tag == "*") + { + continue; + } + + var match = Locales.Offered.FirstOrDefault(l => Matches(l.Tag, tag)); + + if (match is null || match.Tag == Locales.SourceTag) + { + continue; + } + + if (quality > bestScore) + { + best = match; + bestScore = quality; + } + } + + return best; + } + + /// Whether an offered tag answers to what a browser asked for. + /// + /// Both directions, on the language subtag: zh-Hans answers a request for zh and + /// for zh-CN alike, because the script is ours to choose and the region is not something + /// this site varies on. + /// + private static bool Matches(string offered, string asked) => + offered.Equals(asked, StringComparison.OrdinalIgnoreCase) + || Language(offered).Equals(Language(asked), StringComparison.OrdinalIgnoreCase); + + private static string Language(string tag) + { + var dash = tag.IndexOf('-', StringComparison.Ordinal); + + return dash < 0 ? tag : tag[..dash]; + } + + private static string FirstSegment(string path) + { + var trimmed = path.AsSpan().TrimStart('/'); + var slash = trimmed.IndexOf('/'); + + return (slash < 0 ? trimmed : trimmed[..slash]).ToString(); + } + + private static string Rest(string path) + { + var trimmed = path.AsSpan().TrimStart('/'); + var slash = trimmed.IndexOf('/'); + + return slash < 0 ? "/" : trimmed[slash..].ToString(); + } + + /// + /// The page to return to, as a path on this site. + /// + /// + /// It arrives in a form field, so it is whatever the poster typed rather than whatever we + /// rendered — and it is written into a Location header. Same guard as the theme + /// endpoint's, and for the same reasons: //elsewhere.example is a different host wearing + /// a path's clothes and walks straight through a StartsWith('/') check, several browsers + /// read /\elsewhere.example as the same thing, and a CR or LF here is a response-splitting + /// attempt rather than a page anybody asked for. + /// + public static string Back(string? path) => + path is { Length: > 1 } + && path[0] == '/' + && path[1] is not ('/' or '\\') + && path.All(c => c is >= ' ' and < (char)0x7f) + + // Any locale prefix already on it is stripped: the field carries where the reader is, + // and the endpoint decides which language that page is served in. + ? StripLocale(path) + : "/"; + + private static string StripLocale(string path) => + Locales.IsLocaleSegment(FirstSegment(path)) ? Rest(path) : path; + + /// This request's path with its locale prefix removed, for the alternates. + public static string PathWithinLocale(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return context.Request.Path.Value is { Length: > 0 } path ? path : "/"; + } + + /// The absolute address of this request, for a canonical or an alternate. + public static string Absolute(this HttpContext context, string path) + { + ArgumentNullException.ThrowIfNull(context); + + return new Uri(new Uri(UriHelper.BuildAbsolute( + context.Request.Scheme, context.Request.Host)), path).ToString(); + } +} diff --git a/src/MUI.Web/Localization/Locales.cs b/src/MUI.Web/Localization/Locales.cs new file mode 100644 index 0000000..df4232e --- /dev/null +++ b/src/MUI.Web/Localization/Locales.cs @@ -0,0 +1,249 @@ +using System.Collections.Concurrent; +using System.Globalization; + +namespace MUI.Web.Localization; + +/// How far along a locale is, and therefore whether a reader may be sent to it. +/// +/// A locale is offered when its locked strings are translated and reviewed, and not before. +/// The handoff's order of work says it plainly — nothing ships to a reader before the glossary +/// exists — because the failure mode here is not an untranslated button. It is a provenance word +/// that has drifted: "measured", "declared", "not measured", "uncounted" and "unreachable" are five +/// different claims, and a translation engine treats them as stylistic variants of *unavailable*. +/// A reader who cannot tell a game that answered from one that did not has been told something +/// false by the one site whose whole argument is that it never guesses. +/// +public enum LocaleStatus +{ + /// Planned for year one, with nothing translated yet. Never offered. + Planned, + + /// + /// Translated, and therefore offered. + /// + /// + /// + /// There used to be two tiers here and there is no longer a difference worth drawing. A + /// MachineTranslated locale was reachable but never offered: no Accept-Language + /// answer, no hreflang alternate, no default. The idea was that a person should read the + /// glossary before the site sent anybody to a language — but the notice that told readers about + /// the distinction was removed as unnecessary, and what was left was a promise the site made to + /// itself and to nobody else, at the cost of the four languages being undiscoverable. + /// + /// + /// The gate that matters survives and is now the only one: 's locked + /// strings must exist in a locale before it is offered, and a test walks every offered locale to + /// prove they do. That is a fact about the bundle rather than a claim about who read it, which + /// is the kind of gate the rest of this codebase keeps. + /// + /// + Shipped, + + /// + /// A locale that exists to be tested against and is never offered to anybody. + /// + /// + /// Two of them, for two different jobs. The pseudolocale exercises the machinery — routing, + /// fallback, plural selection, and the 1.4x width budget the nav is reviewed against — without + /// anybody claiming it is a language. The Russian canary exists because Chinese cannot fail + /// an agreement bug: it has no grammatical gender, no plural inflection and no case, so a + /// string architecture that is wrong for every inflected language passes review against it. + /// Russian needs four forms of "measured" and three plural categories, so a missing one fails a + /// build instead of reaching a reader. + /// + TestOnly, +} + +/// One interface language, named as its own readers name it. +/// The BCP-47 tag, which is also the path segment. +/// +/// The language's name in itself — Deutsch, Русский, 中文 — never in English and never a flag. +/// +/// Whether a reader may be sent here. +public sealed record Locale(string Tag, string Endonym, LocaleStatus Status) +{ + /// Whether a reader may be sent here by default. + public bool IsOffered => Status is LocaleStatus.Shipped; + + /// Whether a reader who has explicitly chosen this one may be kept in it. + /// + /// Wider than by exactly the review locales, which exist to be tested + /// against and are never offered to anybody. A reader who picks one from the switcher stays in + /// it, which is the difference between offering a locale and honouring a choice. + /// + public bool IsChoosable => Status is LocaleStatus.Shipped or LocaleStatus.TestOnly; + +} + +/// +/// The interface languages, and the boundary around them. +/// +/// +/// +/// Every locale here is left-to-right, and that is a decision rather than an omission. A text +/// run has a direction and a layout has a direction, and only the first is in scope: dir on a +/// <bdi> makes an Arabic game name flow right-to-left inside the cell it occupies and +/// moves nothing else, which is why an Arabic game in an English page is correct permanently. An +/// entire Arabic interface still reading left-to-right is not — the page's reading path +/// fights the text's — so Arabic and Hebrew are render-only: indexed and displayed perfectly, +/// not offered as interface languages. Publishing that boundary is more in keeping with this site's +/// voice than quietly shipping a half-correct Arabic UI, and revisiting it means mirroring and +/// translation together, as one project. +/// +/// +/// Chinese is first because it is plausibly the largest non-English audience for a MU* directory and +/// it stress-tests every typographic finding at once. Chinese and Japanese are separate locales and +/// not one CJK bucket: Unicode unified thousands of Han characters across them and the correct drawn +/// form differs, so lang is what selects between two font families. A reader of either +/// notices the wrong one immediately, and no measurement catches it — the two renderings are exactly +/// the same width. +/// +/// +public static class Locales +{ + /// The language the site is written in, and the one every other falls back to. + public const string SourceTag = "en"; + + /// Where a reader's choice is kept. + /// + /// Read by the server while the page is composed and never by a script, like the theme cookie + /// beside it — this site runs none, and the switcher is a form for that reason. + /// + public const string CookieName = "mui_locale"; + + /// The endpoint the switcher posts to. + public const string Path = "/locale"; + + public const string Field = "locale"; + + public const string ReturnField = "return"; + + /// + /// The seven of year one, plus the two that only a test ever visits. + /// + /// + /// Ordered as the switcher lists them: the source language first, then the rest by the order + /// they were committed to. A list ordered by "most speakers" would be an editorial claim about + /// whose language matters, which is not a measurement and not ours to publish. + /// + public static IReadOnlyList All { get; } = + [ + new("en", "English", LocaleStatus.Shipped), + // Machine-translated and labelled as such on every page. Reachable so the pipeline can be + // exercised against real scripts and real word lengths; not offered, because no person has + new("zh-Hans", "中文", LocaleStatus.Shipped), + new("ja", "日本語", LocaleStatus.Shipped), + new("de", "Deutsch", LocaleStatus.Shipped), + new("nl", "Nederlands", LocaleStatus.Shipped), + + new("ru", "Русский", LocaleStatus.Planned), + new("th", "ไทย", LocaleStatus.Planned), + new("hi", "हिन्दी", LocaleStatus.Planned), + + // Never offered. See LocaleStatus.TestOnly for why these two and not one. + new("qps-ploc", "Pseudo (QA)", LocaleStatus.TestOnly), + new("ru-x-canary", "Русский (CI canary)", LocaleStatus.TestOnly), + ]; + + /// + /// The scripts this site renders correctly and does not offer an interface in. + /// + /// + /// Stated as data rather than left implicit, so the boundary is publishable and testable rather + /// than a thing somebody has to remember. A game whose name is in one of these is indexed, + /// searched, isolated with <bdi> and tagged with its own lang exactly like + /// every other game. + /// + public static IReadOnlyList RenderOnlyScripts { get; } = ["ar", "he"]; + + /// The locales a reader may actually be sent to. + public static IReadOnlyList Offered { get; } = [.. All.Where(l => l.IsOffered)]; + + /// + /// The locales a switcher lists, which in a review build includes the ones that are not + /// languages. + /// + /// + /// + /// The gate stays, and this is not a hole in it. Nothing reaches a reader before its + /// locked strings are translated — is that rule and it is unchanged. What + /// this adds is a way to look at the control: with English the only shipped locale the + /// switcher had nothing to switch between, so it drew nothing at all, and a control nobody can + /// see is a control nobody can review. + /// + /// + /// What it offers in a review build is the pseudolocale and the CI canary, and neither claims to + /// be a translation: one is accented English and the other is machine output that is missing + /// plural forms on purpose. Both are exactly what somebody reviewing the switcher, the routing + /// and the 1.4x width budget wants to click. + /// + /// + /// is a parameter and never a flag on this class. It was a + /// static one that composition wrote on every host start — and a test process starts many hosts, + /// in Development and in Production, so the switcher's contents came from whichever host had + /// started last rather than from the host answering the request. It is asked of the request + /// instead, by : that reaches the environment through + /// the request's own services, so a component with no request behind it — every headless + /// component test here — answers false without needing a web host to be told so. + /// + /// + public static IReadOnlyList Switchable(bool preview) => + [ + .. All.Where(l => l.IsOffered || (preview && l.Status is LocaleStatus.TestOnly)), + ]; + + /// The source locale, which is never missing and never falls back. + public static Locale Source { get; } = All.Single(l => l.Tag == SourceTag); + + /// A tag as a locale, or null when nothing here answers to it. + /// + /// Case-insensitive, because a path segment is whatever somebody typed and /ZH-HANS/games + /// is the same request as /zh-Hans/games. Unknown tags answer null rather than falling + /// back to English silently — the caller decides whether that is a 404 or a redirect, and those + /// are different answers. + /// + public static Locale? Find(string? tag) => tag is null + ? null + : All.FirstOrDefault(l => string.Equals(l.Tag, tag, StringComparison.OrdinalIgnoreCase)); + + /// Whether a path segment looks like one of ours, without deciding anything else. + public static bool IsLocaleSegment(string segment) => Find(segment) is not null; + + private static readonly ConcurrentDictionary Cultures = + new(StringComparer.OrdinalIgnoreCase); + + /// + /// The CLDR data behind a tag — day names, number formats — or the invariant culture where + /// .NET has never heard of it. + /// + /// + /// + /// Not everything a locale says comes out of our bundle, and the day names are the case in + /// point. "Monday" through "Sunday" were an array in a component here, which made a German + /// page render a German sentence around seven English day names — and no translator could have + /// fixed it, because the words were not in a file they are ever sent. CLDR already carries them + /// for every locale this site names and for every locale it might. + /// + /// + /// qps-ploc and ru-x-canary are ours rather than anybody's and asking .NET for + /// them raises. The invariant culture is the right answer for both: neither is a language, and + /// what they exercise is the message machinery rather than a calendar. + /// + /// + public static CultureInfo CultureOf(string tag) + { + ArgumentNullException.ThrowIfNull(tag); + + return Cultures.GetOrAdd(tag, static t => + { + try + { + return CultureInfo.GetCultureInfo(t); + } + catch (CultureNotFoundException) + { + return CultureInfo.InvariantCulture; + } + }); + } +} diff --git a/src/MUI.Web/Localization/MessagePattern.cs b/src/MUI.Web/Localization/MessagePattern.cs new file mode 100644 index 0000000..413da83 --- /dev/null +++ b/src/MUI.Web/Localization/MessagePattern.cs @@ -0,0 +1,498 @@ +using System.Globalization; + +namespace MUI.Web.Localization; + +/// What an argument does with its value. +public enum ArgumentKind +{ + /// {name} — substituted as it stands. + None, + + Number, + Date, + Time, + + /// {n, plural, …} — cardinal agreement. + Plural, + + /// {n, selectordinal, …} — ordinal agreement: 1st, 2nd, 3rd, 4th. + SelectOrdinal, + + /// {gender, select, …} — keyword match on a string. + Select, +} + +/// One piece of a parsed message. +public abstract record MessagePart; + +/// Text, with every quote already resolved. +public sealed record LiteralPart(string Text) : MessagePart; + +/// A bare # inside a plural branch: the count, less the offset. +public sealed record HashPart : MessagePart; + +/// An argument, and whatever it selects between. +public sealed record ArgumentPart( + string Name, + ArgumentKind Kind, + string? Style, + long Offset, + IReadOnlyDictionary Branches) : MessagePart; + +/// +/// One ICU MessageFormat pattern, parsed once and formattable many times. +/// +/// +/// +/// Parsed rather than interpreted, because parsing is when a message can be refused. A +/// pattern that names a branch keyword no plural category uses, or a select with no +/// other, is broken in a way that only shows up for the one reader whose count happens to +/// reach it. Parsing every bundle at startup — and in a test that walks all of them — turns that +/// into a failure somebody sees before a reader does. +/// +/// +/// The whole of MessageFormat 1.0's grammar: simple arguments, number, date and +/// time with their styles and skeletons, plural and selectordinal with +/// offset: and =value matches, select, nesting to any depth, and ICU's +/// apostrophe quoting in its default DOUBLE_OPTIONAL mode. choice is refused: it is +/// deprecated in ICU itself, its syntax is ambiguous with plural, and every use of it is +/// better written as one of the two selectors above. +/// +/// +/// Not MessageFormat 2.0. MF2 reached stable in CLDR 47 and is where this is going, but its +/// syntax is a different language, no .NET implementation exists, and the translation tools this +/// pipeline has to hand strings to — every one of them — speak MF1. Choosing MF2 today would trade +/// a pipeline that works for a spec with nowhere to send the strings. +/// +/// +public sealed record MessagePattern(IReadOnlyList Parts) +{ + /// Parses a pattern, or throws saying where it stopped making sense. + public static MessagePattern Parse(string pattern) + { + ArgumentNullException.ThrowIfNull(pattern); + + var at = 0; + var parsed = ParseMessage(pattern, ref at, inPlural: false, nested: false); + + if (at < pattern.Length) + { + throw new FormatException($"Unexpected '{pattern[at]}' at {at} in: {pattern}"); + } + + return parsed; + } + + /// Every argument this pattern reads, and what it does with each. + /// + /// Walks nested branches too, so a message can be checked against the values a caller will + /// actually pass without rendering it — which is what lets a test assert that every argument a + /// bundle names is one the site supplies. + /// + public IEnumerable Arguments() + { + foreach (var part in Parts) + { + if (part is not ArgumentPart argument) + { + continue; + } + + yield return argument; + + foreach (var branch in argument.Branches.Values) + { + foreach (var nested in branch.Arguments()) + { + yield return nested; + } + } + } + } + + private static MessagePattern ParseMessage(string s, ref int at, bool inPlural, bool nested) + { + var parts = new List(); + var text = new System.Text.StringBuilder(); + + void Flush() + { + if (text.Length > 0) + { + parts.Add(new LiteralPart(text.ToString())); + text.Clear(); + } + } + + while (at < s.Length) + { + var c = s[at]; + + if (c == '}') + { + if (!nested) + { + throw new FormatException($"Unmatched '}}' at {at} in: {s}"); + } + + break; + } + + if (c == '\'') + { + Quote(s, ref at, inPlural, text); + continue; + } + + if (c == '{') + { + Flush(); + at++; + parts.Add(ParseArgument(s, ref at)); + continue; + } + + if (c == '#' && inPlural) + { + Flush(); + parts.Add(new HashPart()); + at++; + continue; + } + + text.Append(c); + at++; + } + + Flush(); + + return new MessagePattern(parts); + } + + /// + /// ICU's apostrophe rules, in the DOUBLE_OPTIONAL mode ICU uses by default. + /// + /// + /// + /// An apostrophe only starts quoted text when it immediately precedes {, }, + /// |, or # inside a plural. Anywhere else it is what it looks like — a literal + /// apostrophe — which is what makes doesn't safe to write without thinking about it. A + /// doubled apostrophe is always one literal apostrophe. + /// + /// + /// This replaces the doubled-brace escape an earlier version carried, which was ambiguous with + /// the syntax it appeared in: every argument ends ...}} when its last branch closes, and + /// a reader treating those two characters as one literal walked straight past the end of the + /// message. That was a real bug and this is the spec's own answer to it. + /// + /// + private static void Quote(string s, ref int at, bool inPlural, System.Text.StringBuilder text) + { + // '' — one literal apostrophe, quoting or not. + if (at + 1 < s.Length && s[at + 1] == '\'') + { + text.Append('\''); + at += 2; + return; + } + + var starts = at + 1 < s.Length + && (s[at + 1] is '{' or '}' or '|' || (inPlural && s[at + 1] == '#')); + + if (!starts) + { + text.Append('\''); + at++; + return; + } + + at++; + + while (at < s.Length) + { + if (s[at] == '\'') + { + // Inside a quote, '' is still one apostrophe; a lone one closes. + if (at + 1 < s.Length && s[at + 1] == '\'') + { + text.Append('\''); + at += 2; + continue; + } + + at++; + return; + } + + text.Append(s[at]); + at++; + } + + // ICU closes an unterminated quote at the end of the pattern rather than erroring. + } + + private static ArgumentPart ParseArgument(string s, ref int at) + { + Skip(s, ref at); + + var name = Name(s, ref at); + + Skip(s, ref at); + + if (Take(s, ref at, '}')) + { + return new ArgumentPart(name, ArgumentKind.None, null, 0, EmptyBranches); + } + + Expect(s, ref at, ',', name); + Skip(s, ref at); + + var type = Word(s, ref at, "an argument type"); + + Skip(s, ref at); + + var kind = type switch + { + "number" => ArgumentKind.Number, + "date" => ArgumentKind.Date, + "time" => ArgumentKind.Time, + "plural" => ArgumentKind.Plural, + "selectordinal" => ArgumentKind.SelectOrdinal, + "select" => ArgumentKind.Select, + + // Deprecated in ICU, ambiguous with plural, and better written as one. Refusing it is + // the whole reason this parser exists rather than a regular expression. + "choice" => throw new FormatException( + $"'choice' is deprecated in ICU and is not supported — use plural or select: {s}"), + + _ => throw new FormatException($"Unknown argument type '{type}' in: {s}"), + }; + + if (Take(s, ref at, '}')) + { + // {n, number} and {d, date} are legal with no style. + return kind is ArgumentKind.Number or ArgumentKind.Date or ArgumentKind.Time + ? new ArgumentPart(name, kind, null, 0, EmptyBranches) + : throw new FormatException($"'{type}' needs branches in: {s}"); + } + + Expect(s, ref at, ',', type); + + if (kind is ArgumentKind.Number or ArgumentKind.Date or ArgumentKind.Time) + { + return new ArgumentPart(name, kind, Style(s, ref at), 0, EmptyBranches); + } + + var offset = 0L; + + if (kind is not ArgumentKind.Select) + { + Skip(s, ref at); + + if (s.AsSpan(at).StartsWith("offset:", StringComparison.Ordinal)) + { + at += "offset:".Length; + Skip(s, ref at); + + var digits = Word(s, ref at, "an offset"); + + if (!long.TryParse(digits, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out offset)) + { + throw new FormatException($"'{digits}' is not an offset in: {s}"); + } + } + } + + var branches = ParseBranches(s, ref at, kind); + + // Every selector needs a fallback, and ICU says so: without one a value nobody anticipated + // has no rendering at all. Refused here rather than at render time, so the message that is + // missing it fails a build rather than one reader's page. + if (!branches.ContainsKey("other")) + { + throw new FormatException($"'{name}' has no 'other' branch in: {s}"); + } + + return new ArgumentPart(name, kind, null, offset, branches); + } + + private static Dictionary ParseBranches(string s, ref int at, ArgumentKind kind) + { + var branches = new Dictionary(StringComparer.Ordinal); + var plural = kind is ArgumentKind.Plural or ArgumentKind.SelectOrdinal; + + while (true) + { + Skip(s, ref at); + + if (at >= s.Length) + { + throw new FormatException($"Unclosed branch list in: {s}"); + } + + if (Take(s, ref at, '}')) + { + return branches; + } + + var selector = Selector(s, ref at); + + if (plural && !selector.StartsWith('=') && !PluralRules.IsCategory(selector)) + { + // A keyword no category uses is dead: nothing ever selects it, in any locale, and + // it is almost always a typo for one that would have. + throw new FormatException( + $"'{selector}' is not a plural category and not an '=' match in: {s}"); + } + + Skip(s, ref at); + Expect(s, ref at, '{', selector); + + branches[selector] = ParseMessage(s, ref at, plural, nested: true); + + Expect(s, ref at, '}', selector); + } + } + + /// A style, which runs to the argument's closing brace and may contain quotes. + private static string Style(string s, ref int at) + { + var text = new System.Text.StringBuilder(); + + while (at < s.Length && s[at] != '}') + { + if (s[at] == '\'') + { + Quote(s, ref at, inPlural: false, text); + continue; + } + + text.Append(s[at]); + at++; + } + + Expect(s, ref at, '}', "a style"); + + return text.ToString().Trim(); + } + + private static readonly IReadOnlyDictionary EmptyBranches = + new Dictionary(StringComparer.Ordinal); + + private static string Name(string s, ref int at) => Word(s, ref at, "an argument name"); + + /// + /// A branch selector: a category keyword, or = and the number it matches exactly. + /// + /// + /// A bare = is a parse error and not a selector. It was accepted — the = + /// was consumed, nothing had to follow it, and the category check above skips anything starting + /// with one — so {n, plural, = {none} other {#}} stored a branch under a key no number + /// written any way can equal. That is exactly the dead branch this parser exists to refuse + /// rather than to keep quietly, and it is the one shape that got past the check. + /// + /// ICU parses an explicit value as a number, so a leading sign and a fraction are both legal + /// after the = and a word is not: =x is a typo for a keyword, and a keyword is + /// what the branch above it would have checked. + /// + /// + private static string Selector(string s, ref int at) + { + var start = at; + + if (at < s.Length && s[at] == '=') + { + at++; + + if (at < s.Length && s[at] == '-') + { + at++; + } + + var digits = at; + + while (at < s.Length && char.IsAsciiDigit(s[at])) + { + at++; + } + + if (at == digits) + { + throw new FormatException($"Expected a number after '=' at {digits} in: {s}"); + } + + if (at < s.Length && s[at] == '.') + { + at++; + + var fraction = at; + + while (at < s.Length && char.IsAsciiDigit(s[at])) + { + at++; + } + + if (at == fraction) + { + throw new FormatException($"Expected a fraction after '.' at {fraction} in: {s}"); + } + } + + return s[start..at]; + } + + while (at < s.Length && (char.IsLetterOrDigit(s[at]) || s[at] is '_' or '-' or '.')) + { + at++; + } + + return at > start + ? s[start..at] + : throw new FormatException($"Expected a selector at {start} in: {s}"); + } + + private static string Word(string s, ref int at, string what) + { + Skip(s, ref at); + + var start = at; + + while (at < s.Length && (char.IsLetterOrDigit(s[at]) || s[at] is '_' or '-' or '.' or '+')) + { + at++; + } + + return at > start + ? s[start..at] + : throw new FormatException($"Expected {what} at {start} in: {s}"); + } + + private static void Skip(string s, ref int at) + { + while (at < s.Length && char.IsWhiteSpace(s[at])) + { + at++; + } + } + + private static bool Take(string s, ref int at, char c) + { + Skip(s, ref at); + + if (at < s.Length && s[at] == c) + { + at++; + return true; + } + + return false; + } + + private static void Expect(string s, ref int at, char c, string after) + { + if (!Take(s, ref at, c)) + { + throw new FormatException($"Expected '{c}' after '{after}' at {at} in: {s}"); + } + } +} diff --git a/src/MUI.Web/Localization/Messages.cs b/src/MUI.Web/Localization/Messages.cs new file mode 100644 index 0000000..6ab2c6b --- /dev/null +++ b/src/MUI.Web/Localization/Messages.cs @@ -0,0 +1,2110 @@ +using System.Globalization; + +using System.Resources; + +namespace MUI.Web.Localization; + +/// +/// Every string the chrome says, keyed by context, in ICU MessageFormat. +/// +/// +/// +/// Stored in resx and rendered by ICU. The two halves solve different problems and the usual +/// .NET arrangement only has one of them. resx is where a translation belongs: the SDK compiles +/// Messages.<culture>.resx into a satellite assembly on its own, every +/// translation-management tool reads and writes the format, and a translator receives a file their +/// software opens rather than a C# dictionary they must not break. What resx cannot do is +/// agreement — {0} substitutes and nothing more — which is why the values are ICU patterns. +/// +/// +/// One message per fact, never a sentence assembled from parts. The strings this replaces +/// were concatenations — a number glued to an English fragment in English word order — and there is +/// nowhere in a concatenation for a translator to intervene without editing markup. Russian needs +/// three plural forms and Arabic six, Chinese needs a measure word, and several languages put the +/// unit before the number. +/// +/// +/// The ids are granular past the point English needs. That is the whole of S7: "measured" is +/// one word here and four in Russian, chosen by what it describes, so provenance.count.measured +/// and provenance.game.measured are separate ids carrying identical English. Collapsing them +/// because the source language cannot tell them apart is exactly how a translation ends up +/// ungrammatical in three places out of four. +/// +/// +/// What is not here. Game names, hostnames, codebase strings, version numbers, protocol +/// acronyms and connect-screen output never enter this file. They are the machine voice, they carry +/// translate="no" in the markup so a browser's own translator obeys too, and translating +/// PennMUSH 1.8.8p0 destroys evidence rather than localizing anything. +/// +/// +public static class Messages +{ + /// + /// The source bundle, compiled in. + /// + /// + /// The same strings as Resources/Messages.resx, and a test walks both to keep them + /// that way. It is here as well because the English is the fallback for every locale and + /// every surface — including the ones rendered with no host behind them — and a fallback that + /// can fail to load is not one. resx is where a translation lives; this is where the + /// source text lives, and the pair is checked rather than trusted. + /// + private static readonly Dictionary English = new(StringComparer.Ordinal) + { + // ── counts, which is where the concatenations were ─────────────────────────────────── + ["facet.count"] = "{count, plural, one {# game} other {# games}}", + ["facet.value.include"] = "{value}, {count, plural, one {# game} other {# games}}, only", + ["facet.value.exclude"] = "{value}, {count, plural, one {# game} other {# games}}, excluded", + ["facet.value.choose"] = "{value}, {count, plural, one {# game} other {# games}}", + ["facet.any"] = "any {facet}, {count, plural, one {# game} other {# games}}", + // Not "every fact measured": the catalogue publishes declared and derived facts too, and + // shows them as such (rule 1 — measured beats declared, and *both are shown*). The summary + // line of a listing that labels a declared count "declared" four rows below cannot claim + // the opposite about the same rows. What is true of every fact is the labelling. + ["listing.total"] = "{count, plural, =0 {No games listed here.}" + + " one {# game, each fact carrying how it was obtained.}" + + " other {# games, each fact carrying how it was obtained.}}", + ["chart.basis"] = "{days, plural, one {# day} other {# days}} measured · {probes, plural, one {# probe} other {# probes}}", + // The day count selects a plural form as the age ladder's does (age.short.days), rather than + // gluing a bare {days} to a literal "d". English does not inflect the abbreviation and + // German does — 1 Tag, 2 Tage — so a translator handed "{days}d" has one slot for two + // forms, and the German satellite duly shipped an English "d" inside a German sentence. + // The unit belongs inside the branch, which is the only place a language can vary it. + ["window.samples"] = "{days, plural, one {#d} other {#d}} · {count, plural, one {# count} other {# counts}}", + ["capabilities.agree"] = "{disagreeing, plural, =0 {None of the {total} disagree.} one {# of {total} disagrees with what the game declares.} other {# of {total} disagree with what the game declares.}}", + + // ── find a game, where the count is the whole point of the page ─────────────────────── + // A sentence and not a bare number. The panel draws "19" at forty pixels for the eye; what + // reaches a screen reader has to say what the nineteen are, because a number announced on + // its own is the one thing on this page nobody can act on. + ["find.matching"] = "{count, plural, =0 {No games match every answer.} one {# game matches every answer.} other {# games match every answer.}}", + ["find.basis"] = "of {listed, plural, one {# listed game} other {# listed games}} · {answers, plural, =0 {no answers given} one {# answer given} other {# answers given}}", + ["find.show"] = "{count, plural, one {Show the one game} other {Show these # games}}", + ["find.drop"] = "drop \"{answer}\" — {count, plural, one {# game} other {# games}}", + ["find.clear"] = "clear answer to: {question}", + + // The page's own copy. Every word of it is here rather than in the markup, including the six + // questions: this is the one surface written in a reader's language rather than in the + // catalogue's, so it is the surface with the most to translate and the least that a + // machine translator could be trusted with. + ["find.title"] = "Find a game", + ["find.kicker"] = "matching all answers", + ["find.noun"] = "{count, plural, one {game} other {games}}", + ["find.clearAll"] = "clear all answers", + ["find.startAgain"] = "start again", + ["find.more"] = "{count, plural, one {# more} other {# more}}", + ["find.answersGiven"] = "answers given", + ["find.wholeListing"] = "the whole listing", + ["find.refused"] = "that query was refused", + ["find.name.label"] = "a name, if you have one", + ["find.name.placeholder"] = "name, or part of one", + ["find.name.submit"] = "Search by name", + + // The six questions, and the answer that un-asks each one. + ["find.q.band"] = "Is anyone playing right now?", + ["find.q.genre"] = "What do you want to play?", + ["find.q.lineage"] = "What kind of game?", + ["find.q.language"] = "In which language?", + ["find.q.client"] = "Anything your client needs?", + ["find.q.dark"] = "Include games that have gone dark?", + ["find.any.band"] = "doesn't matter", + ["find.any.genre"] = "any genre", + ["find.any.lineage"] = "any kind", + ["find.any.language"] = "any language", + ["find.any.client"] = "doesn't matter", + ["find.dark.no"] = "no, only live games", + ["find.dark.yes"] = "yes, show me those too", + ["find.dark.chip"] = "games that have gone dark", + + // ── the client question's options ───────────────────────────────────────────────────── + // One id per capability, each carrying the whole label rather than a gloss to be glued to an + // acronym. The acronym is machine voice and the three words beside it are not, and a + // language that puts the gloss first has nowhere to say so if the two are concatenated. + // `other` is the one that takes the token as an argument, because it is the row for a + // capability this list has never heard of. + ["find.protocol.tls"] = "TLS — encrypted, handshake completed by us", + ["find.protocol.mssp"] = "MSSP — server self-description", + ["find.protocol.mccp"] = "MCCP — compressed output", + ["find.protocol.mxp"] = "MXP — clickable links", + ["find.protocol.gmcp"] = "GMCP — structured client data", + ["find.protocol.msdp"] = "MSDP — structured client data", + ["find.protocol.charset"] = "CHARSET — encoding negotiation", + ["find.protocol.utf8"] = "UTF-8 — non-Latin text renders", + ["find.protocol.ttype"] = "TTYPE — client tells its type", + ["find.protocol.atcp"] = "ATCP — structured client data", + ["find.protocol.msp"] = "MSP — sound triggers", + ["find.protocol.eor"] = "EOR — prompt marking", + ["find.protocol.other"] = "{token} — measured in the handshake", + + // ── the locked provenance words, one id per context ─────────────────────────────────── + ["provenance.count.measured"] = "measured", + ["provenance.game.measured"] = "measured", + ["provenance.capability.measured"] = "measured", + ["provenance.screen.measured"] = "measured", + ["kicker.measured"] = "measured", + ["provenance.count.declared"] = "declared", + ["provenance.game.declared"] = "declared", + ["provenance.capability.declared"] = "declared", + ["kicker.declared"] = "declared", + ["provenance.derived"] = "derived", + ["kicker.derived"] = "derived", + + // ── the four kinds of absence ───────────────────────────────────────────────────────── + ["state.notMeasured"] = "not measured", + ["state.uncounted"] = "uncounted", + ["state.unreachable"] = "unreachable", + ["state.notCounted"] = "not counted", + + // ── the listing's own absences, which are not the game page's ──────────────────────────── + // `state.notCounted` is the glossary's word for a probe that answered without a number, and + // the listing was printing it for a window with no measurement in it at all — three cases + // wearing one word, and the one it wore names a cause. This says the absence and stops. + ["listing.count.none"] = "no count", + ["listing.plain.fromHere"] = "from here", + ["listing.plain.archived"] = "archived", + ["listing.plain.claimed"] = "claimed", + ["random.empty.title"] = "Nothing to pick from", + ["random.empty.body"] = "No game matches that filter. Try {listing}, or {archive}.", + ["random.empty.listing"] = "the whole listing", + ["random.empty.archive"] = "include the archive", + + // ── the words the product rests on ──────────────────────────────────────────────────── + ["term.connected"] = "connected", + ["term.unclaimed"] = "unclaimed", + ["term.claimedByOwner"] = "claimed by its owner", + ["term.stillProbed"] = "still probed", + ["term.typical"] = "typical", + ["term.peak"] = "peak", + + // ── the accessibility promises ──────────────────────────────────────────────────────── + ["a11y.readAsText"] = "read as text", + ["a11y.plainText"] = "plain text", + ["a11y.skipToContent"] = "skip to content", + ["a11y.asciiBanner"] = "ASCII banner: the connect screen of {game}.", + + + // ── site chrome ─────────────────────────────────────────────────────────────────────── + ["nav.catalogues"] = "Catalogues", + ["nav.account"] = "This site and your account", + ["nav.browse"] = "browse", + ["nav.learn"] = "learn", + ["nav.thisSite"] = "this site", + ["nav.menu"] = "menu", + ["nav.games"] = "games", + ["nav.find"] = "find", + ["nav.random"] = "random", + ["nav.archive"] = "archive", + ["nav.reference"] = "reference", + ["nav.ecosystem"] = "ecosystem", + ["nav.rankings"] = "rankings", + ["nav.about"] = "about", + ["nav.submit"] = "submit", + ["nav.submitGame"] = "submit a game", + ["nav.signIn"] = "sign in", + ["nav.yourGames"] = "your games", + ["theme.label"] = "theme", + ["theme.auto"] = "auto", + ["theme.light"] = "light", + ["theme.dark"] = "dark", + ["banner.demo.lead"] = "Demo data.", + ["banner.demo"] = "No database is configured, so this is a fixture. Nothing here was measured.", + ["footer.allGames"] = "all games", + ["footer.archive"] = "archive", + ["footer.declaredByGame"] = "declared by the game", + ["footer.whatChanged"] = "what changed", + + // ── home ────────────────────────────────────────────────────────────────────────────── + ["home.title"] = "A directory of the MU* hobby", + // Same correction as listing.total, on the sentence that makes the claim to a first-time + // reader. The front page cannot say every fact was measured while the row below it wears a + // "declared" chip; what holds of every fact is that it says which of the two it is. + ["home.lede"] = "Every fact carries how it was obtained and how old it is: measured by our " + + "crawler, or declared by the game and marked as such.", + ["home.search.label"] = "Search games by name, theme, codebase or host", + ["home.search.placeholder"] = "search by name, theme, codebase or host", + ["home.search.submit"] = "search", + ["tile.gamesKnown"] = "games known", + ["tile.connectedNow"] = "connected now", + ["tile.answeringUncounted"] = "answering, uncounted", + ["tile.archived"] = "archived", + ["feed.newlyDiscovered"] = "newly discovered", + ["feed.wentDark"] = "went dark — still probed", + ["feed.cameBack"] = "came back", + ["feed.nothingNew"] = "Nothing new.", + ["feed.nothingDark"] = "Nothing went dark.", + ["feed.nothingBack"] = "Nothing came back. We keep knocking.", + ["feed.live"] = "live", + + // ── the listing ─────────────────────────────────────────────────────────────────────── + ["games.title"] = "Games", + ["listing.sortedBy"] = "sorted by {order}", + ["listing.random"] = "random", + ["listing.columns"] = "connected · reached", + ["listing.fromHere"] = "from here", + ["listing.empty.head"] = "Nothing matched.", + ["listing.empty.hint"] = "Try fewer words, or drop a filter.", + ["listing.clearFilters"] = "clear filters", + ["listing.aboutCodebase"] = "about {codebase}", + ["listing.never"] = "never", + ["listing.claimed"] = "claimed by its owner", + ["listing.unknownCodebase"] = "Unknown Codebase", + ["listing.unknownCodebase.title"] = "we could not identify the codebase this game runs", + ["listing.moreProtocols"] = "and {count, plural, one {# more} other {# more}}: {names}", + + // ── the order switch ────────────────────────────────────────────────────────────────── + ["switch.order"] = "Order", + ["switch.window"] = "Window", + ["switch.now"] = "now", + ["switch.typical"] = "typical", + ["switch.peak"] = "peak", + ["switch.name"] = "name", + ["switch.reached"] = "reached", + ["window.7"] = "7 days", + ["window.30"] = "30 days", + ["window.90"] = "90 days", + + // ── the filter panel ────────────────────────────────────────────────────────────────── + ["filters.search.label"] = "Search games", + ["filters.search.placeholder"] = "search games", + ["filters.summary"] = "filters", + ["filters.showing"] = "showing", + ["filters.clearAll"] = "clear all", + ["filters.stopFiltering"] = "— stop filtering by this", + ["facet.anyValue"] = "any", + ["facet.more"] = "more filters ({count})", + ["facet.moreValues"] = "{count, plural, one {# more} other {# more}}", + ["facet.alsoShow"] = "also show", + ["facet.alsoShow.note"] = "Off by default. Neither is a judgement about the game.", + ["facet.archived"] = "archived", + ["facet.adult"] = "adult", + ["facet.archived.state"] = "archived games, {shown, select, true {shown} other {hidden}}", + ["facet.adult.state"] = "games declaring adult content, {shown, select, true {shown} other {hidden}}", + ["facet.countsNote"] = "Counts are games we measured, never estimates.", + ["facet.key.summary"] = "what the badges and the blanks mean", + ["facet.key.blank"] = "A blank is a gap in our measurement, not a no. Each facet spells its own: not identified, not declared, nothing negotiated.", + ["facet.key.zero"] = "A measured zero is a count. An unknown count is not a zero and never sorts as one.", + ["facet.key.openEnded"] = "Open-ended facets list their {count} commonest values. The rest are reachable by search and by URL.", + ["facet.presence.note"] = "Unticked means not measured — not that the game lacks it.", + + // ── facet groups ────────────────────────────────────────────────────────────────────── + ["facet.group.band"] = "activity", + ["facet.group.seen"] = "last seen", + ["facet.group.protocol"] = "protocols offered", + ["facet.group.tls"] = "encrypted", + ["facet.group.charset"] = "encoding", + ["facet.group.codebase"] = "codebase", + ["facet.group.version"] = "version", + ["facet.group.lineage"] = "lineage", + ["facet.group.family"] = "family", + ["facet.group.genre"] = "genre", + ["facet.group.language"] = "language", + + // ── facet values ────────────────────────────────────────────────────────────────────── + ["facet.band.playersNow"] = "connected now", + ["facet.band.activeThisWeek"] = "active this week", + // **Not "uncounted", which this band is not.** `state.uncounted` is a locked glossary term + // meaning "the game answered and we could not read a count", and this rung holds that game + // *and* a game we measured at nought in every hour of the week — opposite facts, one of + // which is a measurement we took (rules 2 and 4). The word was borrowed here before there + // was a facet that meant it, and it now names a control two groups further down the same + // panel that returns a different set. This says what the band actually is: the threshold, + // and no cause. + ["facet.band.quiet"] = "quiet — no count above 0", + ["facet.band.dark"] = "dark — not reached in a month", + ["facet.band.archived"] = "archived", + ["facet.seen.day"] = "in the last 24 hours", + ["facet.seen.week"] = "in the last 7 days", + ["facet.seen.month"] = "in the last 30 days", + ["facet.seen.older"] = "longer ago", + ["facet.seen.never"] = "never reached", + ["facet.unknown.charset"] = "nothing negotiated", + ["facet.unknown.codebase"] = "not identified", + ["facet.unknown.other"] = "not declared", + ["facet.tls.yes"] = "connected over TLS", + ["facet.excluded"] = "not {value}", + ["facet.known.charset"] = "something negotiated", + ["facet.known.codebase"] = "identified at all", + ["facet.known.other"] = "declared at all", + + // ── evidence, and what each word means ──────────────────────────────────────────────── + ["evidence.measured.meaning"] = "we watched this happen", + ["evidence.declared.meaning"] = "the game says so, and we did not check", + ["evidence.derived.meaning"] = "we grouped what the game told us", + + // ── sort orders ─────────────────────────────────────────────────────────────────────── + ["sort.name"] = "name", + ["sort.players"] = "connected now", + ["sort.reached"] = "last reached", + ["sort.medianWeek"] = "typically on · 7 days", + ["sort.medianMonth"] = "typically on · 30 days", + ["sort.medianQuarter"] = "typically on · 90 days", + ["sort.peakWeek"] = "most on at once · 7 days", + ["sort.peakMonth"] = "most on at once · 30 days", + ["sort.peakQuarter"] = "most on at once · 90 days", + ["sort.group.row"] = "on the row now", + ["sort.group.typical"] = "typical", + ["sort.group.peak"] = "peak", + ["sort.unranked.players"] = "Unknown count", + ["sort.unranked.reached"] = "never once reached — not reached long ago", + ["sort.unranked.median"] = "fewer than {minimum} counts in the window, or none at all — not a typical count of zero", + ["sort.unranked.window"] = "nothing we could count in the window — not a game nobody was on", + // {days} selects a plural form here too — same reason as window.samples above. + ["sort.window.median"] = "median {value} · {days, plural, one {#d} other {#d}}" + + " · {count, plural, one {# count} other {# counts}}", + ["sort.window.peak"] = "most {value} at once · {days, plural, one {#d} other {#d}}" + + " · {count, plural, one {# count} other {# counts}}", + + // ── the game page's own headings ────────────────────────────────────────────────────── + ["game.connectScreen"] = "Connect screen", + ["game.connectionsByHour"] = "Connections by hour", + ["game.howMany"] = "How many, over time", + ["game.reachable"] = "Reachable", + ["game.whatChanged"] = "What changed", + ["game.capabilities"] = "Capabilities", + ["game.declaredByGame"] = "Declared by the game", + ["game.referrals"] = "Referrals", + ["game.unclaimed"] = "Unclaimed — everything here was measured.", + ["game.claimed"] = "Claimed by its owner — measured facts below are still ours.", + ["game.claim"] = "Claim this game", + ["game.answeringSince"] = "answering since {date}", + ["game.readAsTextRows"] = "read as text — {count, plural, one {# row} other {# rows}}", + ["capability.column"] = "capability", + ["capability.age"] = "age", + ["capability.offered"] = "offered", + ["capability.silent"] = "silent", + ["capability.absent"] = "absent", + ["capability.denied"] = "denied", + ["capability.claimed"] = "claimed", + ["capability.disagrees"] = "disagrees", + ["capability.whereTheyDisagree"] = "where they disagree ({count})", + + // ── the week of hours, said in words ────────────────────────────────────────────────── + // The heatmap's sentence, its per-day alternative, and the label on every cell. Three + // states and never two: an hour we counted — a measured zero included — an hour that + // answered and produced no count, and an hour nobody has a measurement for. The third + // names no cause in any language, because a probe that failed and an hour we never dialled + // write the same nothing, and a translation that reached for "offline" or "not reachable" + // would file our crawl schedule as a fact about somebody's game. + // + // Every count is an argument rather than a spelled-out word. "Two hours … have" was a + // number, a noun and a verb agreed in English word order, with nowhere in it for a + // translator to stand. + ["activity.cell.counted"] = "{day} {time} — {count, plural, =0 {0 players, measured} one {# player on average} other {# players on average}}", + ["activity.cell.notCounted"] = "{day} {time} — probed, no count could be read", + ["activity.cell.notMeasured"] = "{day} {time} — no measurement in this hour", + + ["activity.none"] = "We have not measured this game's activity yet.", + ["activity.noCount"] = "No hour of the week has produced a player count.", + + // A measured zero everywhere is a measurement, and a strong one. It must not read as an + // absence of data in any language. + ["activity.allZero"] = "Measured every hour and nobody has been on in any of them.", + + // Two ids apiece, because "on Monday" and "across the week" land in different places in + // different languages, and a fragment glued into a sentence is one nobody can move. + ["activity.gap.day"] = "{count, plural, one {# hour on {day} has no measurement yet.} other {# hours on {day} have no measurement yet.}}", + ["activity.gap.week"] = "{count, plural, one {# hour across the week has no measurement yet.} other {# hours across the week have no measurement yet.}}", + ["activity.uncounted.day"] = "{count, plural, one {# hour on {day} answered but produced no count.} other {# hours on {day} answered but produced no count.}}", + ["activity.uncounted.week"] = "{count, plural, one {# hour across the week answered but produced no count.} other {# hours across the week answered but produced no count.}}", + + ["activity.busiest.everyDay"] = "Busiest every day, {window}.", + ["activity.busiest.everyDay.part"] = "Busiest every day, {part}, {window}.", + ["activity.busiest.days"] = "Busiest {days} {window}.", + ["activity.busiest.days.part"] = "Busiest {days} {part}, {window}.", + + ["activity.quiet"] = "Reliably quiet {who}, {window}.", + ["activity.quiet.part"] = "Reliably quiet {who} in the {part}, {window}.", + ["activity.quiet.everyDay"] = "every day", + ["activity.quiet.everyMeasuredDay"] = "every day we could measure", + ["activity.quiet.weekdays"] = "on weekdays", + ["activity.quiet.onDays"] = "on {days}", + + // The same four names in two registers: the busy band takes the plural — "evenings, + // 17:00–21:59" — and the quiet one the singular, "in the evening". English builds the + // second from the first by adding an s, which is a fact about English and not about nouns. + ["activity.part.morning"] = "morning", + ["activity.part.afternoon"] = "afternoon", + ["activity.part.evening"] = "evening", + ["activity.part.smallHours"] = "small hours", + ["activity.parts.morning"] = "mornings", + ["activity.parts.afternoon"] = "afternoons", + ["activity.parts.evening"] = "evenings", + ["activity.parts.smallHours"] = "small hours", + + // A run of days, folded rather than joined on a comma: the separator and the final + // conjunction both belong to a language, and Chinese uses neither of ours. + ["activity.days.list"] = "{list}, {next}", + ["activity.days.pair"] = "{first} and {second}", + + ["activity.sparse.kicker"] = "not enough measurements yet", + ["activity.sparse.none"] = "No hour of the week has a measurement yet.", + ["activity.sparse.uncounted"] = "{count, plural, one {# hour answered and produced no count.} other {# hours answered and produced no count.}}", + ["activity.sparse.wait"] = "The grid appears once every day of the week has one.", + ["activity.sparse.days"] = "{days, plural, one {Measured on # of the seven days so far; the grid appears once every day has an hour in it.} other {Measured on # of the seven days so far; the grid appears once every day has an hour in it.}}", + ["activity.sample.zero"] = "{count, plural, one {# hour measured, all of it at nobody on.} other {# hours measured, all of them at nobody on.}}", + ["activity.sample.peak"] = "{count, plural, one {# hour measured, the busiest {peak} on {day} at {time} UTC.} other {# hours measured, the busiest {peak} on {day} at {time} UTC.}}", + ["activity.sample.more"] = "{count, plural, one {# more hour answered and produced no count.} other {# more hours answered and produced no count.}}", + + ["activity.day.line"] = "{day} — {facts}", + ["activity.day.facts"] = "{first}, {second}", + ["activity.day.allZero"] = "measured at zero all day", + ["activity.day.peak"] = "peak {count} at {time}", + ["activity.day.nobodyOn"] = "nobody on {window}", + ["activity.day.noCount"] = "no count in any hour", + ["activity.day.notMeasured"] = "{count, plural, one {# hour not measured} other {# hours not measured}}", + ["activity.day.notCounted"] = "{count, plural, one {# hour probed but uncountable} other {# hours probed but uncountable}}", + + // The panel's own chrome: the headings of the text alternative, the legend beside the + // drawing, and the key the plain surface prints in place of both. The "not measured" + // column reuses state.notMeasured rather than saying it a second way — the whole panel + // has one word for that hour or it has none. + ["activity.column.day"] = "day", + ["activity.column.quietest"] = "quietest", + ["activity.column.busiest"] = "busiest", + ["activity.column.at"] = "at", + ["activity.column.noCount"] = "no count", + ["activity.caption"] = "Players on by day, in UTC. {window}.", + ["activity.times"] = "times in UTC · {window}", + ["activity.rollingAverage"] = "{weeks, plural, one {#-week rolling average} other {#-week rolling average}}", + ["activity.legend.counted"] = "counted, including a measured zero", + ["activity.legend.notCounted"] = "probed, no count could be read", + ["activity.legend.notMeasured"] = "no measurement in that hour", + ["activity.plain.heading"] = "When people are on (UTC)", + ["activity.key.counted"] = "counted", + ["activity.key.counted.meaning"] = "we got in and read a number, including a measured zero", + ["activity.key.uncounted.meaning"] = "we got in and no number could be read", + ["activity.key.notMeasured.meaning"] = "we have no measurement for that hour", + + // ── the switcher's own chrome, which has to read in the locale being left ───────────── + ["locale.label"] = "language", + ["locale.submit"] = "change language", + + // ═════════════════════════════════════════════════════════════════════════════════════ + // STATIC PAGE COPY — /about, /submit, /account/sign-in + // + // The three pages that were prose in a C# file rather than strings in a bundle, and so were + // still English when the site was asked for German. Nothing here is reworded on the way in: + // the English is byte-for-byte what the page already said, and only its home changed. The + // about page in particular *states* the rules this repository is written from, and a rule + // paraphrased while being moved is a rule quietly rewritten. + // + // Every lead/body pair is two ids for the reason AboutPoint is two fields: the graphical + // page sets the lead in bold and the plain page cannot, so the emphasis is presentational + // and the sentence is not. A translator gets two sentences to move rather than one string + // with markup in the middle of it. + // ═════════════════════════════════════════════════════════════════════════════════════ + + // ── about: the page, and what a fact here is ────────────────────────────────────────── + ["about.title"] = "About mu*index", + ["about.lede"] = "Every game here was measured by a machine that connected to it, and every " + + "value says where it came from and when. This page covers what that proves, what we " + + "get wrong, whose directories we read, and how to make the crawler stop.", + + ["about.measures.heading"] = "What a fact here is", + ["about.measures.declared.lead"] = "Measured beats declared, and both are shown.", + ["about.measures.declared.body"] = "A game's MSSP report is the game describing itself. The " + + "telnet handshake is what we watched it do. Both appear on its page, labelled with how " + + "and when. Where they disagree, we show the disagreement.", + ["about.measures.count.lead"] = "A player count says where it came from.", + ["about.measures.count.body"] = "Either a WHO or DOING read at the connect screen, which we " + + "counted, or the game's own MSSP PLAYERS field, which it published. Never merged.", + ["about.measures.unknown.lead"] = "An answer we cannot read is unknown, never zero.", + ["about.measures.unknown.body"] = "Servers customise their WHO headers freely, and past a " + + "point our parser cannot read one. That is uncountable, its own state. A measured zero " + + "— we got in, nobody was there — is a count, and prints as one.", + ["about.measures.reachable.lead"] = "Reachable, never uptime.", + ["about.measures.reachable.body"] = "We open a socket from one host at intervals. A game we " + + "cannot route to is unreachable and perfectly alive. Nothing here claims a game's " + + "uptime, because nothing here measured it.", + ["about.measures.hour.lead"] = "An hour is counted, uncountable, or not measured.", + ["about.measures.hour.body"] = "The activity grid has three states. The third is empty and " + + "names no cause: an hour we could not reach and an hour we never probed are the same " + + "absence, and neither is that server's downtime.", + + // ── about: what we know we get wrong ────────────────────────────────────────────────── + ["about.limits.heading"] = "What we know we get wrong", + ["about.limits.grace.lead"] = "Archive grace is measured from the day we found you.", + ["about.limits.grace.body"] = "A game that stops answering leaves the default listing after " + + "its grace period: a quarter of the reachable time we probed, floored at 60 days and " + + "capped at 365. A game running since 1995 starts at the floor on the day we discover " + + "it. We import nothing to fill in the years before we arrived.", + ["about.limits.created.lead"] = "We do not credit MSSP CREATED toward that grace.", + ["about.limits.created.body"] = "It is one hand-typed line in a config file, so crediting it " + + "would make the archive threshold gameable. It is shown as a declaration and buys " + + "nothing.", + ["about.limits.claim.lead"] = "Claiming a game earns the ceiling.", + ["about.limits.claim.body"] = "Proving server access is worth the full year of grace, " + + "however long we have been watching.", + ["about.limits.oneHost.lead"] = "Everything here is one host, looking at intervals.", + ["about.limits.oneHost.body"] = "A percentage of reachable time is a fraction of the window " + + "we observed, never of one we did not. No graphic here fills in the rest.", + ["about.limits.deletion.lead"] = "Nothing is ever deleted.", + ["about.limits.deletion.body"] = "Archiving takes a game out of the default listing, the " + + "rankings and the active-today figure, and nothing else. Its page, URL, history and " + + "address survive, it keeps being probed, and one successful probe puts it back.", + + // ── about: what this site will not do ───────────────────────────────────────────────── + ["about.never.heading"] = "What this site will not do", + ["about.never.votes.lead"] = "No votes, stars, ratings or recommendations.", + ["about.never.votes.body"] = "Rankings are computed from measured data only. A directory " + + "ranked by who can mobilise the most clicks describes the campaigning, not the hobby, " + + "and that is what killed the incumbents.", + ["about.never.forums.lead"] = "No forums, reviews, wikis, comments or player profiles.", + ["about.never.forums.body"] = "Orientation material — what a MUSH is, which codebase suits " + + "collaborative roleplay — is written, signed and versioned like the rest of the site.", + ["about.never.names.lead"] = "Player names are never persisted.", + ["about.never.names.body"] = "A WHO reply is parsed in memory for a count and the shape of " + + "the header. The names are not written down; aggregates use a salted hash with a " + + "rotating salt.", + ["about.never.population.lead"] = "No absolute population figure is published.", + ["about.never.population.body"] = "Per-codebase and per-protocol shares ship: a ratio over " + + "the measured set survives the games we cannot count. \"How many people play MU*\" " + + "does not, because that number would not survive being quoted.", + + // ── about: the crawler, and how to make it stop ─────────────────────────────────────── + // Four arguments rather than four concatenations, and none of them is decoration: the + // permitted command list, the MSSP variable and the DNS label and value are all read off the + // objects that consume them, so a page advertising a switch wired to nothing is impossible + // to write. A sentence assembled around them in English word order would have nowhere for a + // translator to put the verb. + ["about.crawler.heading"] = "The crawler, and how to make it stop", + ["about.crawler.probe.lead"] = "A probe is one connection that never logs in.", + ["about.crawler.probe.body"] = "It opens a socket, negotiates telnet options, reads the " + + "connect screen, asks for MSSP by negotiating option 70, sends {commands}, and " + + "disconnects. No character, no login, nothing changed on the far side. A timeout " + + "bounds the session so a wedged probe cannot sit on a connection slot.", + ["about.crawler.delay.lead"] = "CRAWL DELAY wins.", + ["about.crawler.delay.body"] = "A game that states a preferred minimum gap in its MSSP " + + "report gets it, over our own schedule in both directions: 720 hours means monthly, " + + "not weekly. A dark game is still tried for ever at the longer interval, which is how " + + "it re-lists itself when it comes back.", + ["about.crawler.referral.lead"] = "A referred address is verified, never trusted.", + ["about.crawler.referral.body"] = "MSSP lets a game name other games. Every name is resolved " + + "before anything is dialled, and refused unless every address it resolves to is " + + "globally routable. A mixed answer refuses the whole target. Our refusal is filed as " + + "ours and never appears in a game's record as downtime.", + ["about.crawler.screens.lead"] = "Connect screens are shown because they are sent to " + + "everybody.", + ["about.crawler.screens.body"] = "A server paints its connect screen, unauthenticated, to " + + "every anonymous connection. We display it as evidence and label it. Ask and it comes " + + "down.", + ["about.crawler.stop.lead"] = "Say stop, and we stop — three ways.", + ["about.crawler.stop.body"] = "Publish {variable} 1 in your MSSP report, and the probe that " + + "reads it is the last one. Or publish a TXT record at {label}.your.host reading " + + "\"{value}\", which needs no MSSP support and no account here. Or write to a person. " + + "All three are honoured within one crawl cycle, recorded with the date and what we " + + "read, and enforced on the submission form too.", + ["about.crawler.scope.lead"] = "The MSSP field stops that listener; the record stops the " + + "host.", + ["about.crawler.scope.body"] = "MSSP is published by the port that answered, so it speaks " + + "for that port — MU* hosting routinely runs unrelated games on one domain, and one " + + "must not silence its neighbour. A TXT record covers every port unless it names one, " + + "as \"{value}=4201\". Anything there we cannot read as a port list means the whole " + + "host, so \"{value}=all\" works.", + ["about.crawler.dns.lead"] = "The DNS route is the one you can undo without asking us.", + ["about.crawler.dns.body"] = "A TXT record is readable without connecting to a server that " + + "told us not to, so we re-read it before every dial. Delete it and we dial again " + + "within a week. An MSSP field cannot be re-read without doing the thing you asked us " + + "to stop, so MSSP opt-outs and written requests stand until you say otherwise. That " + + "TXT lookup is all an opted-out address gets: it touches your nameserver, never your " + + "game.", + ["about.crawler.stopping.lead"] = "Stopping is not deleting, and it is not downtime.", + ["about.crawler.stopping.body"] = "A game that opts out keeps its page, its address and " + + "everything we measured before it asked. Only new data stops: the activity grid stops " + + "gaining hours and names no cause, because our decision to stop knocking is a fact " + + "about us. It is recorded on the crawl that did not happen, and in the register of " + + "who asked.", + ["about.crawler.unlist.lead"] = "If stopping is not enough, the listing can go too.", + ["about.crawler.unlist.body"] = "Once we have stopped on every address your game answers " + + "on, your dashboard offers one more thing: take it out of the listing, the rankings " + + "and the daily figure. The page and every address it has ever had still answer, and " + + "nothing is deleted — it stops being somewhere a reader arrives by browsing. It needs " + + "a verified claim, because it is a decision about your game and we record who made " + + "it. And a probe undoes it: take your opt-out back, and the next dial that gets an " + + "answer puts you back in the listing without asking us twice.", + + // ── about: who is knocking ──────────────────────────────────────────────────────────── + // Two whole sentences and not one with a branch, because the unannounced case is a + // paragraph explaining a library gap and the announced one is a line. The name is an + // argument in both: it is read off ProbeOptions, so a deployment that configures its own + // gets a page that names it. + ["about.identity.announced"] = "The crawler names itself {name} when a server asks what it " + + "is.", + ["about.identity.unannounced"] = "The crawler is configured to call itself {name} but cannot " + + "yet say so. Its telnet library gives a client no way to set the terminal type, so " + + "your logs see that library's default, and NEW-ENVIRON is answered from the crawler " + + "host's environment. Both are gaps in the library and ours to fix there. Until then, " + + "recognise a probe by its shape: one connection, no login, a short read-only command " + + "set, gone.", + ["about.identity.crawler"] = "crawler", + ["about.identity.contact"] = "contact", + ["about.identity.crawler.line"] = "Crawler: {name}", + ["about.identity.contact.line"] = "Contact: {url}", + ["about.identity.placeholder"] = "— placeholder; this deployment set no contact address", + ["about.identity.placeholder.plain"] = "No contact address is configured, so the one above " + + "is a placeholder and answers nobody.", + + // ── about: where the list of games came from ────────────────────────────────────────── + // The directories' own names and addresses are machine voice and are nowhere in this file. + // What each one gave us, and whether we read it at all, is ours to say and so is here. + ["about.sources.heading"] = "Where the list of games came from", + ["about.sources.addresses.lead"] = "We take addresses. Nothing else.", + ["about.sources.addresses.body"] = "A backfill takes a host and a port. No player counts, no " + + "reachability history, no descriptions, no fields, and no note of which site an " + + "address came from.", + ["about.sources.less.lead"] = "Deliberately less than those sites can give.", + ["about.sources.less.body"] = "Several hold years of dated player counts. Importing that " + + "would fill the heatmaps of the games somebody else was already watching, and rest " + + "this site's central claim on another party's prober.", + ["about.sources.origin.lead"] = "A game's origin is not one fact.", + ["about.sources.origin.body"] = "Any game worth listing appears in several of these " + + "directories, so \"imported from\" would name whichever fetch ran first. That a game " + + "exists is public information; where we read it adds nothing and is the part of " + + "somebody else's work with the least claim to be ours.", + ["about.sources.etiquette.lead"] = "Reading somebody's site is still reading somebody's " + + "site.", + ["about.sources.etiquette.body"] = "We ask for a bulk export or a documented endpoint before " + + "scraping, read robots.txt first, and rate-limit scrapes hard. A source that needs its " + + "maintainer's say-so is not fetched until a person can state they were asked.", + + // Two states and never one word for both: a directory we chose not to fetch is a different + // fact from one we could not, and the badge is the only place a reader meets the difference. + ["about.source.read"] = "read — addresses only", + ["about.source.withheld"] = "not read — awaiting permission", + + ["about.source.tintinMssp.note"] = "One page, one request. Published by a crawler that " + + "connects to each game and prints what it read.", + ["about.source.tintinMsdp.note"] = "The same crawler's MSDP listing. Nearly a subset of its " + + "MSSP sibling, read for the few addresses it reaches that the other does not.", + ["about.source.mudConnector.note"] = "Publishes its whole catalogue on one page, so reading " + + "it costs a single request. Our largest source of addresses, and of no measurements.", + ["about.source.mudStats.note"] = "One index page and one page per world, so a scrape rather " + + "than an export. On 30 July 2026 we fetched 143 of their pages, fifteen seconds apart " + + "and honouring robots.txt, but before anyone had written to them. That should not " + + "have happened. The gate now takes a person willing to state the maintainer was " + + "asked.", + ["about.source.mudVerse.note"] = "Implemented, tested, never run. The strongest source here " + + "on every axis except permission, and nothing will be fetched until somebody has " + + "written to them.", + + // ── about: licence ──────────────────────────────────────────────────────────────────── + ["about.licence.heading"] = "Licence", + ["about.licence.code.lead"] = "The code is MIT.", + ["about.licence.code.body"] = "The site, the crawler and the parsers are open source under " + + "the MIT licence.", + ["about.licence.open.lead"] = "The licence for the data is an open question.", + ["about.licence.open.body"] = "A separate decision from the code's, and not yet taken. Treat " + + "the terms below as this deployment's current answer, not the project's settled " + + "position. A rival directory taking the whole catalogue is a success condition here, " + + "so whatever is settled will not stand in the way of one.", + ["about.licence.codeLabel"] = "code", + ["about.licence.dataLabel"] = "data, as this deployment serves it", + ["about.licence.creditLabel"] = "credit as", + ["about.licence.code.line"] = "Code: {licence}", + ["about.licence.data.line"] = "Data: {licence}", + ["about.licence.credit.line"] = "Credit as: {credit}", + ["about.licence.deployment"] = "(what this deployment serves. The project's own answer is " + + "still open.)", + + // ── submit a game ───────────────────────────────────────────────────────────────────── + // A host, a port, and nothing else. The form has no name box and no description box, so + // every word on this page is ours rather than a submitter's, and all of it belongs here. + ["submit.title"] = "Submit a game", + ["submit.lede"] = "Tell us where a game is. A host and a port is the whole form; everything " + + "else on this site is measured by our own crawler.", + ["submit.host.label"] = "Host", + ["submit.port.label"] = "Port", + ["submit.host.hint"] = "mud.example.org, or paste mud.example.org:4201 and leave the port " + + "empty", + ["submit.button"] = "Submit", + ["submit.noCatalogue"] = "Submitting needs a database, and this site is running on the demo " + + "fixture. There is no crawl registry to write into, so the form is absent rather than " + + "quietly doing nothing.", + ["submit.notHere"] = "Not here", + ["submit.what.heading"] = "What happens to an address", + ["submit.what.resolve"] = "We resolve the address before dialling it, and refuse anything " + + "that resolves off the public internet. That is a decision about our own socket, " + + "never a fact about a game.", + ["submit.what.optOut"] = "If whoever runs that host has asked us not to crawl it, we will " + + "not take the address, whoever submits it. A stranger cannot put your game back on " + + "this site.", + ["submit.what.schedule"] = "If it answers, we read what the server says for itself and keep " + + "reading it on its own schedule, for ever. An address only has to be given once.", + ["submit.what.claim"] = "Nothing appears on the site until somebody proves they run it. " + + "Claiming takes a passkey and one line published on the game itself.", + ["submit.what.duplicate"] = "An address we already have collapses onto the existing entry. " + + "Sending it twice makes no second listing and brings no probe forward.", + + // The answers. Every one takes {address} as an argument rather than opening with it, because + // a language that puts the subject elsewhere has nowhere to say so if the address is glued + // to the front of an English sentence. The word for an address we could not read is its own + // id: it is a noun phrase standing where a hostname would, and it inflects. + ["submit.answer.thatAddress"] = "that address", + ["submit.accepted.heading"] = "In the registry.", + ["submit.accepted.sentence"] = "{address} will be dialled on the next crawl cycle, then on " + + "its own schedule for ever. It appears here once somebody proves they run it — come " + + "back to this form with the same address and it will hand you the link.", + ["submit.unclaimed.heading"] = "We have it, unclaimed.", + ["submit.unclaimed.sentence"] = "{address} is one we already measure. It stays off the site " + + "until somebody proves they run it. If that is you, this is the way in.", + ["submit.known.heading"] = "We already have that one.", + ["submit.known.sentence"] = "{address} is a game we already measure. Nothing was created and " + + "nothing was changed.", + ["submit.knownAddress.heading"] = "We already have that address.", + ["submit.knownAddress.sentence"] = "{address} is already known to us. Nothing was created " + + "and nothing was changed.", + ["submit.queued.heading"] = "Already waiting.", + ["submit.queued.sentence"] = "{address} is in the crawl registry and has not answered yet. " + + "Sending it again does not bring it forward: a target keeps its own schedule, so " + + "nobody can hurry us at somebody else's server.", + ["submit.malformed.heading"] = "Not an address we can dial.", + ["submit.malformed.sentence"] = "A host needs a dot or a colon in it, and a port is a number " + + "between 1 and 65535. Fill in both boxes, or paste mud.example.org:4201 into the " + + "first.", + ["submit.undialable.heading"] = "We cannot dial that.", + ["submit.undialable.sentence"] = "Three things produce this answer for {address}: the name " + + "may not resolve, it may resolve off the public internet, or whoever runs that host " + + "may have asked us to stay away. We deliberately do not say which, because answering " + + "that for a stranger maps a network from outside it. Nothing was recorded about the " + + "address; the decision was ours and it is filed as ours.", + ["submit.tooMany.heading"] = "Enough for now.", + ["submit.tooMany.sentence"] = "This form is rate-limited by sender, and you have hit the " + + "bound. Come back in an hour. Nothing was lost — anything we took is already in the " + + "registry.", + ["submit.link.claim"] = "claim this game", + + // ── signing in, which is a passkey and nothing else ─────────────────────────────────── + ["account.signIn.title"] = "Sign in", + ["account.signIn.preview"] = "Sign in with a passkey to claim a game you run. There is no " + + "password to lose and none to steal.", + ["account.signIn.noDatabase"] = "Claiming needs a database, and this site is running on the " + + "demo fixture. There is nothing to sign in to.", + ["account.signIn.passkey.lead"] = "Sign-in is a passkey.", + ["account.signIn.passkey.body"] = "Your device or password manager holds the private key; we " + + "hold only the public half. No password, no email.", + ["account.signIn.button"] = "Sign in with a passkey", + ["account.signIn.script"] = "The one page here that needs JavaScript. Passkeys cannot work " + + "without it.", + ["account.register.heading"] = "No account yet?", + ["account.register.lede"] = "You need one only to claim a game you run. Pick a name to be " + + "known by — a label beside your claim, not a real name.", + ["account.register.name.label"] = "Name", + ["account.register.name.placeholder"] = "e.g. corvid-admin", + ["account.register.button"] = "Create an account with a passkey", + ["account.store.heading"] = "What we store", + ["account.store.name"] = "The name you chose.", + ["account.store.keys"] = "The public key of each passkey you register, and what your device " + + "called it.", + ["account.store.claims"] = "Which games you have claimed, and when.", + ["account.store.note"] = "No email address, no password, no IP log tied to your account. " + + "Lose every passkey and you can publish a fresh claim token on your game and start " + + "again: the game is the proof, not the account.", + + // ── dates, ages and provenance — the two shapes on nearly every page ────────────────── + // Appended as one block on purpose: three other surfaces are appending to this file at the + // same time, and a marked section at the end is a merge that adds rather than one that + // collides. + // + // The words come from CLDR and the order comes from here. A month name is + // CultureInfo's — see Locales.CultureOf, and see the day names in the heatmap, + // which are the same job — but the *arrangement* of day, month and year is not something a + // .NET format string can express for a language it is never sent. Japanese writes + // 2026年7月30日 and German puts a point after the day; both are one edit to this pattern and + // neither is reachable through ToString("d MMM yyyy"). + ["date.absolute"] = "{day} {month} {year}", + + // UTC is named rather than implied, and it is not a word to translate. Every time on this + // site is UTC because a crawler's clock is the only one it has, and a reader in another zone + // who is not told cannot tell whether 14:02 is theirs. The 24-hour spelling is the site's + // and not the locale's for the same reason: one zone, one clock, one shape. + ["date.stamp"] = "{date} {time} UTC", + + // ── the age ladder, in three registers ──────────────────────────────────────────────── + // Three families over the same seven rungs, and the English is identical in two of them. + // That is the point. A bare duration is a duration; "how long ago did we last confirm this + // value" and "how long has this game been unreached" are two different questions, and a + // language that answers them with one phrasing can still choose to — while one that needs + // "vor 2 Wo." for the first and "seit 2 Wo." for the second has somewhere to say so. + // + // Every rung is a real ICU plural even where English has one form, because the branch a + // language actually needs is not knowable from the source text. `#` prints the number. + ["age.short.now"] = "now", + ["age.short.minutes"] = "{count, plural, one {#m} other {#m}}", + ["age.short.hours"] = "{count, plural, one {#h} other {#h}}", + ["age.short.days"] = "{count, plural, one {#d} other {#d}}", + ["age.short.weeks"] = "{count, plural, one {#w} other {#w}}", + ["age.short.months"] = "{count, plural, one {#mo} other {#mo}}", + ["age.short.years"] = "{count, plural, one {#y} other {#y}}", + + // How long ago we last confirmed a value. The freshest rung is a word rather than a + // duration — "now ago" was a real bug, and giving the rung its own id is what makes it + // unwritable rather than merely fixed. + ["age.ago.now"] = "just now", + ["age.ago.minutes"] = "{count, plural, one {#m ago} other {#m ago}}", + ["age.ago.hours"] = "{count, plural, one {#h ago} other {#h ago}}", + ["age.ago.days"] = "{count, plural, one {#d ago} other {#d ago}}", + ["age.ago.weeks"] = "{count, plural, one {#w ago} other {#w ago}}", + ["age.ago.months"] = "{count, plural, one {#mo ago} other {#mo ago}}", + ["age.ago.years"] = "{count, plural, one {#y ago} other {#y ago}}", + + // How long since the game was last reached. Identical English, different question — and + // never "offline" or "down" in any language: we measured a socket from one vantage point + // and a game with a routing problem to our host is unreachable and perfectly alive. + ["age.dark.now"] = "just now", + ["age.dark.minutes"] = "{count, plural, one {#m ago} other {#m ago}}", + ["age.dark.hours"] = "{count, plural, one {#h ago} other {#h ago}}", + ["age.dark.days"] = "{count, plural, one {#d ago} other {#d ago}}", + ["age.dark.weeks"] = "{count, plural, one {#w ago} other {#w ago}}", + ["age.dark.months"] = "{count, plural, one {#mo ago} other {#mo ago}}", + ["age.dark.years"] = "{count, plural, one {#y ago} other {#y ago}}", + + // The