Noxzedhttps://noxzed.de/Mon, 22 Apr 2024 00:00:00 +0200Timeloop (Post-Mortem)https://noxzed.de/2024/timeloop-post-mortem/<p>Die Idee kommt im Kern von dem Video »<a href="proxy.php?url=https://www.youtube.com/watch?v=CBawCe6du3w">Clock Blockers</a>«, in dem nach jeder Runde ein neuer Spieler zur Partie hinzugefügt wird und dieser in das bereits passierte Spielgeschehen eingreifen, und dieses somit verändern und erweitern kann.</p> <p>Das Lernziel war es, ein rundenbasiertes Spiel zu erstellen, welches die Bewegungen der einzelnen Figuren speichert und diese wieder abspielen kann. </p> <p>Geplant hatte ich hier mit mehreren unterschiedlichen Spielfiguren, welche auch unterschiedliche Fähigkeiten haben. Die eine wäre im Nahkampf besser, während die andere bereits aus der Ferne eine Attacke ausführt oder sogar verschiedene Zauber ausführen kann. Fokusiert wurde jedoch erstmal der Nahkampf.</p> <p>Am ersten Prototypen arbeitete ich im September 2023 in Godot. Da auch noch mit den Tiles von <a href="proxy.php?url=https://pixeldungeon.watabou.ru/">Pixel Dungeon</a>. Durch die Komplexität des Abspeicherns der Aktionen und mit den genaueren Steuerungsmöglichkeiten (nur Steuerkreuz oder auch Aktionsbuttons, etc), war hier nach kurzer Zeit die Luft raus und ich habe mich anderen Themen zugewandt.</p> <p><a href="proxy.php?url=/2024/gamejam-timeloop-post-mortem/timeloop-godot-1.png"><img alt="Screenshot von Timeloop in Godot mit Pixel Dungeon Tiles 1" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2024/timeloop-post-mortem/timeloop-godot-1.png"></a> <a href="proxy.php?url=/2024/gamejam-timeloop-post-mortem/timeloop-godot-2.png"><img alt="Screenshot von Timeloop in Godot mit Pixel Dungeon Tiles 2" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2024/timeloop-post-mortem/timeloop-godot-2.png"></a> <a href="proxy.php?url=/2024/gamejam-timeloop-post-mortem/timeloop-godot-3.png"><img alt="Screenshot von Timeloop in Godot mit Pixel Dungeon Tiles 3" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2024/timeloop-post-mortem/timeloop-godot-3.png"></a> <a href="proxy.php?url=/2024/gamejam-timeloop-post-mortem/timeloop-godot-4.png"><img alt="Screenshot von Timeloop in Godot mit Pixel Dungeon Tiles 4" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2024/timeloop-post-mortem/timeloop-godot-4.png"></a></p> <p>Nachdem ich auf die Game Engine <a href="proxy.php?url=https://github.com/kitao/pyxel">Pyxel</a> gestoßen bin, wollte ich mit dieser (und meinen Python-Kenntnissen), der Idee nochmal eine Chance geben und einen Prototypen dafür erstellen.</p> <p>Die Pixel-Dungon Tiles konnte ich in der Engine nicht direkt verwenden, weswegen ich auf die Pyxel-Bordmittel zurückgegriffen habe. Die Grafik sollte möglichst minimalistisch sein. Aus dem Grund wurde die Pyxel Standard-Farbpalette verwendet und mal schnell ein paar Tiles und verschiedene Figuren dazu erstellt. Die Tilegröße war hierbei 8x8. </p> <p>Als ich dann mit Pyxel einen erneuten Anlauf durchgeführt hab, ging die Umsetzung relativ leicht von der Hand. Auch die Implementation des Aktionsspeichers, wurde schneller und auch leichter umgesetzt. Vermutlich hat es geholften, dass die mit Python durchgeführt wurde und nicht mit GDScript und ich somit die Datenstruktur leichter aus meinen Gedanken im Code abbilden konnte.</p> <p>Die kleine Tilesize von 8x8 hatte zu Folge, dass ich viel weniger Zeit mit dem Erstellen von Tiles verbracht habe. Die Grafik wurde dann auf das wesentliche heruntergebrochen und der Fokus konnte auf die Funktionen des Spiel liegen und weniger auf dessen Aussehen.</p> <p>Nachdem der Aktionsspeicher fertig Implementatiert war und ich auch ein paar Runden spielen konnte, hab ich leider schnell festgestellt, dass die Idee so als Spiel nicht taugt. Der Spieler, der zuerst am Zug ist, hat einen extrem starken Vorteil, den der zweite Spieler nicht ausgleichen kann. Zudem kommt hinzu, dass es leicht möglich ist, mit der ersten Spielfigur länger am Spawn zu verweilen, um in der nächsten Runde von der zweiten Spielfigur in der Bewegung unterbrochen zu werden. Dadurch ist es dem Spieler auch wieder möglich die Figur frei zu bewegen, da diese nicht die gespeicherten Bewegungen aus dem Aktionsspeicher verwendet.</p> <p><a href="proxy.php?url=/2024/post-mortem-timeloop/timeloop-pyxel.gif"><img alt="GIF welches den schnellen Durchlauf einer Partie zeigt" class="image-process-image-full" src="proxy.php?url=https://noxzed.de/2024/timeloop-post-mortem/timeloop-pyxel.gif"></a></p> <p>Vielleicht hätte ich pro Runde ein Zeitlimit einfügen oder die Spawns der Figuren weiter voneinander entfernen müssen. Das schien mir aber alles den Aufwand nicht wert, wenn die Grundidee einfach nicht wie geplant/gewünscht funktioniert.</p>Cyd NoxzedMon, 22 Apr 2024 00:00:00 +0200tag:noxzed.de,2024-04-22:/2024/timeloop-post-mortem/2024-timeloopgamedevgodotpyxelpost-mortemHesperos (Post-Mortem)https://noxzed.de/2023/hesperos-post-mortem/<p>Nach den vielen kleineren Projekten 2023, mit einem Umfang von einem Monat, wollte ich das Nächste doppelt so lange durchführen.</p> <p>Die Idee war eine Mischung aus Tower-Defence und Ressourcenabbau. Man verteidigt seine Basis vor Wellen von Angreifern, entweder direkt mit der Spielfigur oder mit gebauten Geschützen. Letzteres muss man sich jedoch erst durch das Abbauen von Blöcken in der Umgebung der Basis verdienen.</p> <p>Vom Aussehen wollte ich die Grafiken wieder komplett selbst erstellen. Um das auch stemmen zu können, war der Plan einiges über Pixelart zu lernen. Einen Einstieg habe ich über <a href="proxy.php?url=https://lospec.com/pixel-art-tutorials">Lospec</a> einige gute Einsteigerartikel zum Thema gefunden und dort auch einige Farbpaletten gesucht. Um den Schwierigkeitsgrad zu senken, habe ich mich für zwei Farben entschieden und eine Tilegröße von 8x8 - also ziemlich niedrige Anforderungen.</p> <p>Der Fortschritt im August hat aber meinen Erwartungen nicht ganz entsprochen. Einige merkwürdige Bugs und auch ein gedankliches Hin und Her bezüglich der Steuerung, haben dazu geführt, das ich das Projekt vernachlässigt habe.</p> <p>Bereits in der zweiten Augusthälfte wollte ich an dem Spiel nicht mehr weiterarbeiten. Ungeklärte Fragen, über den weiteren Entwicklungsverlauf, sammelten sich an.<sup id="sf-hesperos-post-mortem-1-back"><a href="proxy.php?url=#sf-hesperos-post-mortem-1" class="simple-footnote" title="Darunter: können die Türme nur in 4 Richtungen schießen oder sich um 360° drehen?">1</a></sup></p> <p>Wobei ich immernoch glaube, dass das Spielkonzept interessant ist, habe ich mich entschlossen das Spiel erstmal beiseite zu legen.</p> <p><a href="proxy.php?url=/2023/hesperos-post-mortem/hesperos-1.png"><img alt="Hesperos 1" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/hesperos-post-mortem/hesperos-1.png"></a> <a href="proxy.php?url=/2023/hesperos-post-mortem/hesperos-2.png"><img alt="Hesperos 2" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/hesperos-post-mortem/hesperos-2.png"></a> <a href="proxy.php?url=/2023/hesperos-post-mortem/hesperos-3.png"><img alt="Hesperos 3" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/hesperos-post-mortem/hesperos-3.png"></a> <a href="proxy.php?url=/2023/hesperos-post-mortem/hesperos-4.png"><img alt="Hesperos 4" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/hesperos-post-mortem/hesperos-4.png"></a> <a href="proxy.php?url=/2023/hesperos-post-mortem/hesperos-5.png"><img alt="Hesperos 5" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/hesperos-post-mortem/hesperos-5.png"></a> <a href="proxy.php?url=/2023/hesperos-post-mortem/hesperos-6.png"><img alt="Hesperos 6" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/hesperos-post-mortem/hesperos-6.png"></a></p><ol class="simple-footnotes"><li id="sf-hesperos-post-mortem-1">Darunter: können die Türme nur in 4 Richtungen schießen oder sich um 360° drehen? <a href="proxy.php?url=#sf-hesperos-post-mortem-1-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedFri, 15 Dec 2023 00:00:00 +0100tag:noxzed.de,2023-12-15:/2023/hesperos-post-mortem/2023-hesperosgamedevgodotgamejampost-mortemGamejam Julihttps://noxzed.de/2023/gamejam-juli/<p>Im <a href="proxy.php?url=/2023/gamejam-rocket/">letzten Jam</a> hab ich mich für ein paar Stunden wieder mit prozeduraler Generierung ablenken lassen. Deshalb wollte ich im nächsten Jam einen kleinen Dungeon Crawler erstellen, welcher die Levels anhand eines Seeds generiert.</p> <p>Somit war das Mindestziel wie folgt:</p> <ul> <li>Spielfigur bewegt man mit der Tastatur</li> <li>Blickrichtung via Maus</li> <li>Finden und Sammeln von Schlüsseln</li> <li>verschlossene Türen versperren den Weg in die nächste Ebene</li> <li>Gegner sehen den Spieler und machen Schaden bei Berührung</li> </ul> <p>Grafisch wollte ich zunächst alles selbst machen. Habe dann doch für die Figuren auf vorhandene Tilesets zurückgegriffen. Die Dungeon-Tiles hab ich dann doch selbst gemacht und auch einige Gegenstände (im Grunde nur die Schlüssel).</p> <p>Für die Generierung der Dungeon habe ich anstelle eines bisher mir bekannten Algorithmus ein anderes Konzept verfolgt und die Räume anhand von Prefabs (aus einer farbcodierten PNG) zufällig nebeneinander angeordnet und die Gegenstände / Monster abhängig von der aktuellen Ebene (bzw. des Spielfortschritts) darin verteilt.</p> <p>Da alle Räume den gleichen Grundbauplan haben und potentiell in alle 4 Richtungen mit anderen Räumen verbunden werden können, musste ich hier keine Unterscheidungen treffen. Um ein bisschen mehr Variation zu den 8 Räumen zu bekommen, gibt es von allen jeweils noch (dynamisch) gespiegelte Versionen.</p> <p>Aus Zeitgründen musste ich einige Features weglassen. Ein Monstertyp ist eigentlich ein bisschen wenig. Auch kann man eine anderen Gegenstände außer Schlüssel finden und verschlossene Türen gehen auch nur in den nächsten Level. Tränke mit verschiedenen Wirkungen wären auch ganz nett gewesen, genau wie Boss Gegner.</p> <p>Was ich jedoch noch implementiert habe war ein Field-Of-View, welches vom Spieler kreisförmig ausgeht und die Sicht durch Wände/Türen behindert wird. Leider hat dieses Feature zur Folge, dass das Spiel im Browser nur noch einen schwarzen Bildschirm anzeigt. Warum das der Fall ist, habe ich auf die Schnelle nicht herausfinden können.</p> <p>Zwischenzeitlich hab ich das Konzept verfolgt, dass man die Blickrichtung nicht mit der Maus steuert, sondern die Welt sich um den Spieler dreht und dieser relativ gesehen immer nach oben/vorne blickt. Der nächste Schritt wäre vermutlich dann MODE7 gewesen. Dann hätte ich aber auch alle Sprites auf eine top-down-Ansicht ändern müssen. Das hab ich dann lieber gestrichen. Sowas wäre vermutlich für ein Rennspiel interessant.</p> <p>In den letzten Entwicklungsstunden habe ich dann noch den Highscore implementiert. Dieser speichert lediglich die Zahl der tiefsten bisher erreichten Ebene ab.</p> <p><a href="proxy.php?url=/2023/gamejam-juli/july23-1.png"><img alt="Menu" class="image-process-image-full" src="proxy.php?url=https://noxzed.de/2023/gamejam-juli/july23-1.png"></a> <a href="proxy.php?url=/2023/gamejam-juli/july23-2.png"><img alt="Gameplay 1" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-juli/july23-2.png"></a> <a href="proxy.php?url=/2023/gamejam-juli/july23-3.png"><img alt="Gameplay 2" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-juli/july23-3.png"></a> <a href="proxy.php?url=/2023/gamejam-juli/july23-4.png"><img alt="Gameplay 3" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-juli/july23-4.png"></a> <a href="proxy.php?url=/2023/gamejam-juli/july23-5.png"><img alt="Gameplay 4" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-juli/july23-5.png"></a> <a href="proxy.php?url=/2023/gamejam-juli/july23-6.png"><img alt="Gameplay 5" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-juli/july23-6.png"></a> <a href="proxy.php?url=/2023/gamejam-juli/july23-7.png"><img alt="Gameplay 6" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-juli/july23-7.png"></a></p>Cyd NoxzedSun, 20 Aug 2023 00:00:00 +0200tag:noxzed.de,2023-08-20:/2023/gamejam-juli/2023-julygamedevgodotgamejamGamejam Rockethttps://noxzed.de/2023/gamejam-rocket/<p>Nachdem ich einfache Achtwege-Bewegung im <a href="proxy.php?url=/2023/gamejam-marz/">letzten Jam</a> umgesetzt hatte, wollte ich mich nun ein bisschen tiefer mit der Physik-Engine auseinander setzen, die Godot zu bieten hat. Zum Einstieg habe ich mir hier lediglich Schwerkraft vorgenommen.</p> <p>Hierzu wollte ich über die Osterfeiertage einen kleinen Prototypen erstellen, welcher lediglich Schwerkraft enthält. Da ein Sidescroller trotzdem einige Elemente hat, die mehr Aufwand erfordern, dachte ich an einen simplen Klon eines Spiel, welches ich in den 2000ern mal gespielt hatte: <a href="proxy.php?url=https://archive.org/details/CrazyGravity_1020">Crazy Gravity</a>. </p> <p>Um das Spiel grafisch Ansprechender zu machen, wollte ich die Sprites diesmal selbst erstellen. Um darin nicht zu viel Zeit zu verschwenden entschied ich mich das ganze einen einfachen Schwarz-Weiß-Look zu geben. Damit ich den Schwarzton ggf. ein bisschen variabel entschärfen kann und der Kontrast nicht zu stark ist, habe ich in den Sprites lediglich weiße und transparente Pixel verwendet.</p> <p>Ziel des Spiels war es, innerhalb von kürzester Zeit, mit einem der Gravitation beeinflussten Rakete, Kisten von anderen Startrampen zu holen und diese zum Startpunkt der Karte zu bringen. Da die Rakete mit jedem Schub Treibstoff verliert, muss man zusätzlich ab und an zu einer Tankstelle fliegen. Wenn alle Kisten abgeholt sind, ist die Karte zu Ende und man bekommt die benötigte Zeit angezeigt.</p> <p>Damit ich hier schnell Levels bauen kann, musste ich mich auch gleich mal mit dem Autotiling von Godot beschäftigen. Für den Anfang sollte hier ein einfaches 2x2-Tiling reichen. Auch hier habe ich einfache Linien verwendet, um die Grenzen des Levels darzustellen. Mit dem Tilemap-Editor in Godot konnte ich dann ganz schnell ein paar Maps erstellen.</p> <p>Für den ganzen Prototypen habe ich nur ein paar Tage über Ostern benötigt. Hier könnte man tatsächlich von einem Gamejam sprechen, da ich hier auch pro Tag mehrere Stunden darin invenstieren konnte. </p> <p>Generell bin ich mit dem Ergebnis ganz zu frieden. Die Steuerung funktioniert gut und das Spielprinzip ist kurzweilig. Jedenfalls ein schönes Beispiel was man aus einem guten Spielkonzept auf die Schnelle rausholen kann.</p> <p><a href="proxy.php?url=/2023/gamejam-rocket/rocket-1.png"><img alt="Screenshot of Rocket 1" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-rocket/rocket-1.png"></a> <a href="proxy.php?url=/2023/gamejam-rocket/rocket-2.png"><img alt="Screenshot of Rocket 2" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-rocket/rocket-2.png"></a> <a href="proxy.php?url=/2023/gamejam-rocket/rocket-3.png"><img alt="Screenshot of Rocket 3" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-rocket/rocket-3.png"></a> <a href="proxy.php?url=/2023/gamejam-rocket/rocket-4.png"><img alt="Screenshot of Rocket 4" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-rocket/rocket-4.png"></a> <a href="proxy.php?url=/2023/gamejam-rocket/rocket-5.png"><img alt="Screenshot of Rocket 5" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-rocket/rocket-5.png"></a> <a href="proxy.php?url=/2023/gamejam-rocket/rocket-6.png"><img alt="Screenshot of Rocket 6" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-rocket/rocket-6.png"></a></p>Cyd NoxzedSat, 27 May 2023 00:00:00 +0200tag:noxzed.de,2023-05-27:/2023/gamejam-rocket/2023-rocketgamedevgodotgamejamGamejam Märzhttps://noxzed.de/2023/gamejam-marz/<p>Ich hatte mal wieder Lust mich mit Spieleentwicklung auseinander zusetzen. Um dieses mal einen anderen Weg einzuschlagen, hab ich mich entschlossen mich in eine fertige Engine einzuarbeiten. Meine Wahl fiel dann auf <a href="proxy.php?url=https://godotengine.org/">Godot</a>. Primär aus den Gründen, dass es Freie Software ist und ich Builds für unterschiedliche Plattformen erstellen kann.</p> <p>Parallel dazu habe ich mich mit den Konzept von <a href="proxy.php?url=https://de.wikipedia.org/wiki/Game_Jam">Gamejams</a> auseinandergesetzt. Deren Ziel ist es innerhalb kurzer Zeit ein Spiel fertig zu stellen. Was für mich bedeutet eine Deadline zu haben und entsprechend Features wegstreichen zu müssen um rechtzeitig fertig zu werden.</p> <p>Die meisten Gamejams laufen so über ein Wocheende und in dieser Zeit kann/sollte man vollständig am Jam arbeiten. Zeitlich ist das für mich jedoch aktuell keine Option, ich kann vermutlich maximal 30 Minuten täglich (wenn überhaupt) dafür aufwenden. Bei den Gamejams, welche über einen Monat oder länger gehen, fühle ich mich wahrscheinlich auch schnell abgehängt. Nach langlaufenden speziellen Gamejams für erwachsene Berufstätige habe ich jedoch nicht gesucht, so ein Kommitment wollte ich dann doch nicht eingehen.</p> <p>Also habe ich mir das Ziel gesetzt im Laufe des gesamten März ein Spiel zu schreiben, in welcher ich mir die Grundzüge der Godot-Engine aneignen kann. Darunter auch Bewegung der Spielfigur und Szenenwechsel.</p> <p>Entschieden habe ich mich dann für ein einfaches Rätselspiel, bei dem man in jedem Raum jeweils ein Rätsel lösen muss, um in den nächsten Raum fortschreiten zu können. Es soll darin Elemente wie Schiebe- oder Schalterrätsel geben und keine handlungsfähigen Gegner.</p> <p>Also habe ich eine Reihe verschiedener Räume gebaut, in welchen man ein oder mehrere Rätsel lösen muss. Mal ist es Kisten auf eine Druckplatte verschieben, mal Knöpfe in einer bestimmten Reihenfolge drücken. Viele der Level sind jedoch eher Klone von Sokoban-Rätsel, welches sich durch das freiere Bewegen der Spielfigur teilweise anders lösen lassen.</p> <p>Weil die Grafik ziemlich schlicht gehalten ist (größtenteils besteht alles nur aus einfachen geometrischen Figuren), hab ich besonders darauf geachtet, dass die einzelnen Levels und deren Elemente leicht zu lesen und zu verstehen sind.</p> <p>Das ganze Spiel lässt sich in ca. 5-10 Minuten durchspielen.</p> <p>5 Tage vor dem Ende des Jams habe ich das Spiel zum ersten Mal jemanden testen lassen. Die Kritik war erschütternd. Viele Elemente des Spiels waren nervig und unschön gelöst. Das hab ich während der Entwicklung schon geahnt, aber nicht weiter Berücksichtigt. Wenn man ein Level 100x spielt, verliert man den Blick des Anfängers sehr leicht aus den Augen.</p> <p>Ich habe versucht in den letzten Tage die gröbsten Schnitzer zu entfernen, um das Spiel schöner spielbar zu machen.</p> <p>Rückblickend muss ich sagen, dass die Idee eines Zeitlimits bzw. einer Deadline enorm dazu beigetragen hat, zumindest etwas fertig zu machen. Zu schnell verliert man sich bei sowas in Kleinigkeiten, welche einem zwar in der Entwicklung spaß bereiten, jedoch keinen wirklichen Mehrwert schaffen. Der Fokus sollte immer bei der Erstellung der Kernelemente liegen. Polieren und Erweitern kann man im Anschluss immer noch.</p> <p>In Godot selbst, kam ich durch die ganze Dokumentation Online, sowie den Massen an Tutorials gut rein. Auch habe ich in der Zeit die Vorteile der Architektur von Godot kennen gelernt. Wenn man von einem ECS kommt, wirkt der Ansatz mit den Szenen bei Godot erstmal befremdlich. Sobald man jedoch sich stärker damit auseinander gesetzt hat fällt es einem leicht die Vorteile davon für sich zu nutzen. Vermutlich bin ich hier jedoch erst am Anfang.</p> <p><a href="proxy.php?url=/2023/gamejam-marz/march23-1.png"><img alt="Menü" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-marz/march23-1.png"></a> <a href="proxy.php?url=/2023/gamejam-marz/march23-2.png"><img alt="Level 1" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-marz/march23-2.png"></a> <a href="proxy.php?url=/2023/gamejam-marz/march23-3.png"><img alt="Level 2" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-marz/march23-3.png"></a> <a href="proxy.php?url=/2023/gamejam-marz/march23-4.png"><img alt="Level 3" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-marz/march23-4.png"></a> <a href="proxy.php?url=/2023/gamejam-marz/march23-6.png"><img alt="Level 4" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-marz/march23-6.png"></a> <a href="proxy.php?url=/2023/gamejam-marz/march23-7.png"><img alt="Level 5" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2023/gamejam-marz/march23-7.png"></a></p>Cyd NoxzedMon, 24 Apr 2023 00:00:00 +0200tag:noxzed.de,2023-04-24:/2023/gamejam-marz/2023-marchgamedevgodotgamejamDezentrale Flatfile Datenbank (Teil 2)https://noxzed.de/2023/dezentrale-flatfile-datenbank-teil-2/<p>Im ersten Post zur <a href="proxy.php?url=/2021/dezentrale-flatfile-datenbank/">Dezentrale Flatfile Datenbank</a> habe ich bereits mal den Gedanken geäußert eine dezentrale flatfile Datenbank zu benötigen.</p> <p>Seit dem veröffentlichen des Posts sind einige Gedanken hierzu noch dazugekommen, beziehungsweise habe ich einige Implementationsversuche unternommen, um ein eigenes System auf die Beine zu stellen.</p> <p>Einsatzbereich war primär erneut der Feedreader und eine Verwaltung der einzelnen Feeds geräteübergreifend zu ermöglichen.</p> <h2>Grundidee</h2> <p>Während eine SQLite-Datei eine komplette Datenbank enthält und man diese auch als normale Datei synchronisieren lassen kann, würde es jedoch spätestens dann zu Problemen führen, wenn man die Datei vor einer Synchronisierung von zwei Rechnern aus modifiziert. Der Sync würde dann mit einem Mergekonflikt fehlschlagen.</p> <p>Bei anderen Flatfile-Datenbank-Systemen (wie CSV, recutils oder maildir) würden bei umfangreicheren Änderungen auch solche Probleme auftreten. Zumindest könnte man bei solchen Plaintext-Formaten leichter mit diff oder ähnlichen Tools arbeiten. Manuell müsste man jedoch trotzdem tätig werden, was es zu vermeiden gilt.</p> <p>Ziel soll es sein, eine Plaintext Datenbank zu haben, welche das Mergen vollständig übernimmt, ohne dass man dabei einen zusätzlichen Software-Stack aufsetzen muss. Die Synchronisierung selbst soll mit jeder beliebigen Dateisynchronisierung (Syncthing, rsync, Nextcloud, git, etc) durchgeführt werden können.</p> <h2>Mergekonflikte</h2> <p>Deshalb sind Mergekonflikte auf ein Minimum zu reduzieren. Da Mergekonflikte generell bei Änderungen einzelner Dateien auftreten, war der Gedanke, alle Informationen in einzelne Dateien zu speichern. Der Primärschüssel ist dann ein Pfad auf einen Ordner und der Ordner enthält dann für jede singulare Information eine einzelne Datei. Diese enthalten dann die jeweiligen Werte. Wenn die Datei nicht existiert, dann wird für deren Inhalt ein Standardwert (Leerstring oder NULL) angenommen.</p> <p>Bei einem Mergekonflikt kann dann zb die neuste Datei den globalen Stand bestimmen, je nachdem wie das Synchronisierungstool so etwas automatisch auflösen kann.</p> <h2>Warum separate Dateien für jede Information?</h2> <p>Es gibt einige Formate, mit denen man ein Entity vollständig in einer Datei erfassen kann. Hierzu zählen JSON, Markdown-Frontmatter oder auch INI. Diese müssten jedoch auch gut durchsuch-/filterbar sein, was abhängig von der Formatierung nicht unbedingt gegeben ist, bzw. man die Dateien vorher noch parsen müsste.</p> <p>Bei unterschiedlichen Formatierungen wäre ein lösen von Mergekonflikten im Rahmen des Syncs auch nicht mehr möglich.</p> <h2>Aufbau der Datenbank Struktur</h2> <p>Primärschlüssel: Als Primärschlüssel dient lediglich der Pfad der Entität innerhalb des "Datenbank"-Ordners. Feld: Der Feldname entspricht der Datei innerhalb des Entity-Ordners. Tabellen: Gibt es in dem Sinne nicht, siehe "Unterscheidung von Entitätstypen"</p> <h2>Unterscheiden von Entitätstypen</h2> <p>Bei einer relationellen Datenbank gibt es unterschiedliche Tabellen, welche unterschiedliche Daten beinhaltne können. In einer Flatfile Datenbank kann man dies abhängig vom Ordnerpfad, von den einzelnen Dateien einer Entität oder von einer Typen-Datei innerhalb der Entity abhängig machen.</p> <h2>Suchen, Filtern und Sortieren</h2> <p>Abhängig davon, ob man nur nach Entitäten suchen möchte oder nach einem bestimmten Inhalt, würden sich die klassichen Unixtools wie <code>find</code> oder <code>grep</code> anbienten. <code>Glob</code> wäre hier sicherlich auche eine hilfreiche Implementation, um alle Felder mit einem bestimmten Namen in einem Ordner zu finden.</p> <p>Umfangreichere Filter könnten auch mit pipe-Verknüpfungen in <code>grep</code> realisiert werden.</p> <p>Sortieren ist mit <code>sort</code> auch mit den Unixtools möglich und vermutlich sinnvoll. Auch vergleichen von Daten könnte innerhalb einer Subshell (mit gt, eq, lt) stattfinden.</p> <h2>Nachteile</h2> <p>Die Nachteile dieser Datenhaltung wären zunächst, dass ein größerer Platz auf der Festplatte eingenommen wird, da jedes einzelne Datenbankfeld den ganzen Informationsoverhead einer Datei beinhaltet, sowie die fehlende Indizierung der Daten, welches standardmäßig eine Datenbank mit übernimmt.</p> <p>Auch das durchsuchen der "Datenbank", mit <code>grep</code>, ohne Indizierung würde mehr CPU-Zeit beanspruchen.</p> <h2>Implementation</h2> <p>Kommend sollte ich mich vermutlich mal an eine entsprechende Implementation versuchen, um zu testen ob das alles so funktioniert wie ich mir das aktuell denke und ob eine dezentrale Datenhaltung in so einer Datenbank überhaupt praktikabel ist.</p> <p>Für Vorschläge, Links und Anregungen zu dem Thema kann man mir gerne eine kurze Mail schreiben.</p>Cyd NoxzedFri, 24 Feb 2023 00:00:00 +0100tag:noxzed.de,2023-02-24:/2023/dezentrale-flatfile-datenbank-teil-2/codingcodingflatfile databasedeRelease: Webferea 2.4.0https://noxzed.de/2022/release-webferea-240/<p>Über den Winter hat sich in der Entwicklung von Webferea einiges getan. </p> <p>Der HTML-Code der Feed-Einträge wird vor der Ausgabe nun viel stärker gefiltert und nur bestimmte Tags und Attribute im HTML-Code verbleiben. Das hilft dabei, dass Feed-Einträge die Struktur der Webferea-Seite nicht negativ beeinflussen kann. Die Filter für interne Links, Iframes und Bilder wurden überarbeitet und verlinken nun korrekt, auch wird man nun nach dem Login auf die letzte besuchte Seite weitergeleitet. </p> <p>Die umfangreichste Änderung des neuen Releases ist jedoch das neue Theme. Im Vorfeld wurde die Templates und die zugehörigen statischen Dateien in einen neuen Ordner verschoben, damit das Anlegen von neuen Templates leichter durchführbar ist. </p> <p>Neben dem alten Standardtheme gibt es jetzt ein w3css-Theme, welches auf dem CSS <a href="proxy.php?url=https://www.w3schools.com/w3css/">w3css</a> aufbaut. W3css ist ein kleines und hübsches Style-Framework, welches ohne Javascript auskommt und ganz viele Beispiele mitliefert. Angepasst habe ich das erneut auf die mobile Nutzung.</p> <p>Die statischen Dateien sind Größer als im Default-Theme, aber ein Aufruf der Liste oder der Einträge ist kleiner.</p> <p>Für die Icons wird bei dem Theme font-awesome 4.7 verwendet und keine UTF-8 Icons, dadurch sehen diese auch browserübergreifend gleich aus. </p> <p>Der Lesestatus eines Eintrags wird jetzt im localStorage des Browsers gespeichert und entsprechend am Eintrag mit angezeigt (als Fortschrittsbalken oben beim Eintrag oder als Tag in der Liste). Wenn man erneut den Eintrag öffnet springt man entsprechend wieder dort hin. </p> <p>Zusätzlich kann das Theme noch folgendes: </p> <ul> <li>Wechsel zwischen Dark- und Lightmode</li> <li>Wechsel der Font zwischen Serif-Schrift und San-Serif-Schrift</li> <li>Gleichzeitiges Bearbeiten von Einträgen in der Liste</li> </ul> <p>Sonst sind wieder einige Fixes eingeflossen, welche mir beim täglichen Benutzen aufgefallen sind.</p> <p>Zu finden ist das Projekt aktuell auf <a href="proxy.php?url=https://github.com/CydNoxzed/webferea2">Github</a>.</p> <p>Hier noch ein paar Screenshots:</p> <p><a href="proxy.php?url=/2022/release-webferea-240/webferea-v2-4_1.png"><img alt="W3css-Theme Liste im Lightmode" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2022/release-webferea-240/webferea-v2-4_1.png"></a> <a href="proxy.php?url=/2022/release-webferea-240/webferea-v2-4_2.png"><img alt="W3css-Theme Liste im Darkmode" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2022/release-webferea-240/webferea-v2-4_2.png"></a> <a href="proxy.php?url=/2022/release-webferea-240/webferea-v2-4_3.png"><img alt="W3css-Theme Hauptmenü im Darkmode" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2022/release-webferea-240/webferea-v2-4_3.png"></a> <a href="proxy.php?url=/2022/release-webferea-240/webferea-v2-4_4.png"><img alt="W3css-Theme Actionmenü auf Artikelseite im Lightmode" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2022/release-webferea-240/webferea-v2-4_4.png"></a></p>Cyd NoxzedFri, 18 Mar 2022 00:00:00 +0100tag:noxzed.de,2022-03-18:/2022/release-webferea-240/webfereaprojectswebfereaDezentrale Flatfile Datenbankhttps://noxzed.de/2021/dezentrale-flatfile-datenbank/<p>Aktuell ist die Aktualisierung von <a href="proxy.php?url=/tag/webferea/">Webferea</a> abhängig von meinem Hauptcomputer, da ich meine Webferea-Instanz mit diesem synchronisiere. Eine Synchronisation zwischen mehr als einem Master würde nicht funktionieren, da die Rücksynchronisation nur den Lesestatus der einzelnen Posts überträgt, nicht jedoch die einzelnen Feeds an sich oder deren Position in der Feed-Ordnerstruktur.</p> <p>Wie im <a href="proxy.php?url=/2021/webferea-2/">initialen Webferea Post</a> angedeutet, fehlen mir bei Liferea<sup id="sf-dezentrale-flatfile-datenbank-1-back"><a href="proxy.php?url=#sf-dezentrale-flatfile-datenbank-1" class="simple-footnote" title="Liferea ist mein aktuell genutzer Feedreader, welcher auch als Basis für die Webferea-Synchronisation dient.">1</a></sup> einige Funktionen.</p> <p>Am Einfachsten wäre es vermutlich die Datenbank in Webferea auf dem Server direkt zu verwalten (inkl. Aktualisieren und so) und meine Rechner greifen jeweils nur darauf zu. Als Client würde dann vermutlich pur der Browser zum Einsatz kommen und keine ressourcenschonendere native Alternative, was mir nicht so ganz zusagt. Zudem könnte ich dann gleich auf bereits etablierte Programme wie <a href="proxy.php?url=https://tt-rss.org/">Tiny Tiny RSS</a> oder <a href="proxy.php?url=https://miniflux.app/">Miniflux</a> zugreifen ohne die Arbeit einer Eigenentwicklung. </p> <p>Die Dezentralität spielt für mich hier aber eine entscheidente Rolle. Ich möchte unabhängig von einer Serververbindung meine Feeds direkt auf den Clients verwalten, sodass ich diese auch Offline lesen kann. </p> <p>Um nicht direkt bei allen Sachen bei Null anfangen zu müssen, habe ich mich erstmal mit der Synchronisation auseinander gesetzt. Da ich meine Rechner und Smartphone bereits via <a href="proxy.php?url=https://syncthing.net/">Syncthing</a> verbunden habe und diese so auch bereits Dateien austauschen, war meine Überlegung einfach darauf zurückzugreifen und die Synchronisation von einem darauf spezialisierten Programm übernehmen zu lassen. </p> <p>Bei einer simplen Flatfile-Datenbank würden die einzelnen Werte jedoch CSV-ähnlich abgespeichert werden. Wenn hier dann Änderungen an den zwei Rechnern erfolgt, ohne zwischendrin zu synchronisieren, müsste ich hier manuell die Dateien dann mergen. Das wäre nicht zielführend. </p> <p>Die nächste Überlegung war, die einzelnen Posts aus den Feeds jeweils in separate Dateien zu speichern, diese würden dann ja ohne Probleme synchronisiert werden. Damit es keine Kollisionen gibt benenne ich die Dateien jeweils anhand einer zufällig generierten UUID. Falls Posts zweimal gefetched und eingetragen werden, dann lasse ich diese jeweils automatisch anhand deren Titel abgleichen. Wenn ein Post bereits gelesen ist und der andere noch nicht, wird der bereits gelesene behalten und der andere gelöscht. Das ist aber echt umständlich zu implementierende Logik, die eigentlich von der Synchronisation erledigt werden sollte und nicht vom Feedreader selbst.</p> <p>Eigentlich brauche ich doch nur eine dezentrale Flatfile-Datenbank. Kann doch nicht der erste sein, der sowas implementieren möchte.</p> <p>Flatfile-Datenbanken gibts ja so einige, nennen will ich hier jedenfalls noch <a href="proxy.php?url=https://www.gnu.org/software/recutils/">Recfiles</a> aus den GNU Recutils. Aber auch die sehen keine Synchronisation vor.</p> <p>Zwischenzeitlich hab ich mich noch mit dokumentenorientierten Datenbanken beschäftigt. Gerade <a href="proxy.php?url=https://couchdb.apache.org/">CouchDB</a>-Instanzen können sich wohl sehr gut untereinander synchronisieren. Leider ist hier wieder ein neuer Stack für die Synchronisierung zuständig und ich kann diese nicht einfach wie normale Dateien auch synchen.</p> <p>Als Entwickler ist mir hierbei auch ein paar mal <a href="proxy.php?url=https://git-scm.com/">git</a> in den Sinn gekommen. Synchronisieren und Mergen von Flatfiles ist ja quasi dessen Kernkompetenz. Aber da man hier auch wieder abstand von den eigentlichen Synctools nimmt, hab ich mich damit nicht näher beschäftigt. Immerhin ist git ja für Sourcecode und nicht für Datenbanken gedacht. Zumal ich hier auch bei einem Merge manuell tätig werden müsste.</p> <p>Neulich bin ich dann jedoch auf einen interessanten Artikel gestoßen: <a href="proxy.php?url=https://www.kenneth-truyers.net/2016/10/13/git-nosql-database/">Git as a NoSql database</a> von Kenneth Truyers, welcher den Gedanken vertieft und einige Beispiele für eine Implementation mitgibt. </p> <p>Momentan ist die Variante mit git (bare-repo oder Checkout) aktuell mein Favorit. Hier müsste ich dann aber Logiken implementieren, welche bei einem Mergekonflikt die Datenbank wieder gerade ziehen (nicht gerade ein einfacher Task).</p> <p>Wobei ich wirklich interessiert daran wäre, ob es bereits Implementationen einer dezentralen Flatfile Datenbank gibt. Wäre für Links hierzu dankbar.</p><ol class="simple-footnotes"><li id="sf-dezentrale-flatfile-datenbank-1"><a href="proxy.php?url=https://lzone.de/liferea/">Liferea</a> ist mein aktuell genutzer Feedreader, welcher auch als Basis für die Webferea-Synchronisation dient. <a href="proxy.php?url=#sf-dezentrale-flatfile-datenbank-1-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedSat, 30 Oct 2021 00:00:00 +0200tag:noxzed.de,2021-10-30:/2021/dezentrale-flatfile-datenbank/codingcodingflatfile databasedeGeminify the mailhttps://noxzed.de/2021/geminify-the-mail/<p><em>This is a crosspost of my post of the same title on <a href="proxy.php?url=https://bbj.envs.net/">bbj.envs.net</a></em>.<sup id="sf-geminify-the-mail-1-back"><a href="proxy.php?url=#sf-geminify-the-mail-1" class="simple-footnote" title="BBJ is a bulletin board which can be accessed with the tilde ssh account.">1</a></sup></p> <p>A few months ago in tilde.chat/#gemini I was part in an interesting conversation about tranfering the concepts and goals of gemini to email.</p> <p>Despite the fact that email is a greatly decentralized standard in the online communication, it has a few historical flaws that are mostly impossible to patch. For example the lack of encryption per default.</p> <p>"Email" is basicaly a push protocol and not a fetch one like gemini. The goal would be to design a protocol that can be used to push fully encrypted data (or messages) from one user (or system) to another.</p> <p>If you already know a project or idea in that direction, feel free to <a href="proxy.php?url=/contact/">contact me via email</a> and send me a link.</p> <p>For a push orientated protocol the receiver of the message should be online most of the time and adressable, therefore the client-server-architecture should be used. Even thou it could be the case that a server is not always online (eg. if its solar powered).</p> <p>A basic path of a message could be the following:</p> <p>Client A → Outbox A → Server A → Server B → Inbox B → Client B</p> <p>There are different challenges to tackle, and most of them are just rethinking existing protocols:</p> <ul> <li>Encryption and the Public Key Infrastructure</li> <li>Envelope- and Message-Format</li> <li>Mailbox-Format</li> <li>Address / Identifier</li> <li>S2S-Protocol</li> <li>C2S-Protocol</li> </ul> <h2>Encryption and the Public Key Infrastructure</h2> <p>I'm not an expert on encryption and only know the basics of asymmetric encryption. For this very reason I don't know how to design such a system that is capable of fetching and validating the public key(s) of a specific user, to be able to send him an encrypted message. There are a lot of pitfalls I don't know.</p> <h2>Envelope- and Message-Format</h2> <p>The envelope itself contains 3 parts:</p> <ul> <li>Receiver address (eg. [email protected])</li> <li>the message (encrypted)</li> <li>the signature of the sender server of the above data</li> </ul> <p>The server can not see the content of the message or who is the sender address.</p> <p>The message can only be decrypted by the client and contains header and a body. The body can contain multiple segments like a message or attachments.</p> <p>Theirfore the server can not handle spam protection or redirect- or sorting rules. This can only be done by the client that can encrypt the messages.</p> <p>Besides normal attachments that contains the file directly, it should be possible to reference/link to files that are too big to attach. Next to the link of the file there should be a decryption key and an expiration date provided, that way the files can be downloaded to a later time. (This could be used as a tracking pixel, but I'm not sure how to work around the filesize of a message with big attachments).</p> <h2>Mailbox-Format</h2> <p>Right now MAILDIR or MBOX let have the server full control over the messages and can see a lot of the content/metadata of the mails. This should not be possible.</p> <p>Due to the full encryption, the server can not spam check the mails or filter them by any criteria. This needs to be done by the clients.</p> <p>The user mailbox could have 3 directories:</p> <ul> <li>inbox: all messages that were received by the server for this user (the user removes the message from the inbox after moving it into the encrypted data directory)</li> <li>outbox: messages that the server should send to the receiver (the server deletes the message after it was send)</li> <li>data: all files that are shared by the users clients (messages, encryption keys, etc) but fully encrypted</li> </ul> <h2>Address / Identifier</h2> <p>The address must contain at least two parts: the user identifier and the server identifier. eg:</p> <ul> <li>[email protected] (normal address)</li> <li>[email protected] (disposable address for alice)</li> <li>groupconversation/[email protected] (address the user alice in a group conversation or mailinglist)</li> </ul> <h2>S2S-Protocol</h2> <p>This would possibly be the hardest part of all, because a server needs to be sure to receive messages only from trusted sources to prevent spam.</p> <p>The sender server connects to the receiver server and identifies itself, and the receiver needs to validates this identity. Maybe over TXT records in the DNS or the servers have their own key-pair for signing and encrypting.</p> <h2>C2S-Protocol</h2> <p>The protocol provides access to the mailbox of the user. Perhaps SSH/SFTP could be enough to handle multiple clients from the same user? Messages itself could be written offline and sync to the servers outbox possibly days later, there should not occure errors on syncing the mailbox between server and the clients.</p> <p>Those a just a few thoughts on this topic. If you have a different approach or other ideas I'm pleased to hear them. Most of the ideas above are probably naive, but I think they could head into the right direction.</p> <p>If there is a better platform (or mailinglist) for this thought experiment, please let me know.</p><ol class="simple-footnotes"><li id="sf-geminify-the-mail-1"><a href="proxy.php?url=https://tilde.town/wiki/socializing/bbj.html">BBJ</a> is a bulletin board which can be accessed with the tilde ssh account. <a href="proxy.php?url=#sf-geminify-the-mail-1-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedThu, 15 Jul 2021 00:00:00 +0200tag:noxzed.de,2021-07-15:/2021/geminify-the-mail/smallwebsmallwebgeminienEinfach zum Abschluss bringenhttps://noxzed.de/2021/weird-v1/<p>Nachdem ich jetzt jahrelang immer on und off daran rumwerkel, wurde es mal an der Zeit das Projekt zu einem Ende zu bringen. Von den Initial vorgenommenen <a href="proxy.php?url=/2021/was-fur-ein-spiel-soll-es-sein/">Zielen</a> habe ich in dem Zug einige ausgeklammert, da ich das MVP<sup id="sf-weird-v1-1-back"><a href="proxy.php?url=#sf-weird-v1-1" class="simple-footnote" title="wp:Minimum_Viable_Product">1</a></sup> in möglichst einer Woche fertigstellen wollte.</p> <p>Die letzte große Änderung an dem Spiel war im Herbst letzten Jahres, als ich den Kern nochmal komplett auf <a href="proxy.php?url=/2021/entity-component-system/">ECS</a> umgestellt habe. Das war (geschätzt) vermutlich bereits das fünfte Rewrite des Kernes seitdem ich mit dieser Spieleidee gestartet bin. Um die Sache diesmal ein bisschen zu beschleunigen habe ich mich dazu entschlossen erstmal alles Grafische auszuklammern und das dann irgendwann später nachzuholen. Sowohl die Karte, als auch die Akteure auf der Karte, bestanden aus simplen geometrischen Figuren<sup id="sf-weird-v1-2-back"><a href="proxy.php?url=#sf-weird-v1-2" class="simple-footnote" title="Größtenteils große und kleine Quadrate und Kreise.">2</a></sup> und das alles sah auch dementsprechend abschreckend aus.</p> <p>Angefangen hatte ich damals™ mit einer Größe von 64x64 Pixel von einzelnen Kacheln und Sprites. Da war ich auch noch in der strengen Top-Down Ansicht.</p> <p><a href="proxy.php?url=/2021/weird-v1/weird-1.jpg"><img alt="Prototyp mit Top-Down Ansicht" class="image-process-image-full" src="proxy.php?url=https://noxzed.de/2021/weird-v1/weird-1.jpg"></a></p> <p>Weil ich aber hierfür kaum Grafiken gefunden hab und die Kacheln für die Wand wegen der Perspektive ziemlich unschön aussehen, bin ich vor ein paar Jahren mal zur einer weniger strengen Top-Down Sicht gewechselt. Bei der Gelegenheit habe ich auch die Pixelgröße von 64 auf 16 verkleinert, um einen Pixel-Art-Look zu bekommen.</p> <p><a href="proxy.php?url=/2021/weird-v1/weird-2.jpg"><img alt="Prototyp in Pixelart" class="image-process-image-full" src="proxy.php?url=https://noxzed.de/2021/weird-v1/weird-2.jpg"></a></p> <p>Da hab ich aber auch viel Zeit damit verschwendet schöne Grafiken zu finden, deshalb bin ich da dann bei der Umstellung auf ECS auch dazu übergegangen eine möglichst simple Darstellung zu nutzen, damit ich mich auf die Funktionalitäten fokusieren kann.</p> <p><a href="proxy.php?url=https://noxzed.de/2021/weird-v1/weird-3.mp4">&gt; Video mit simpler Grafik</a></p> <p>Die Grafik wurde bei allen Kommentatoren jedoch als "bescheiden" bezeichnet, was ich fairerweise nachvollziehen kann. Aus dem Grund habe ich mich nochmal hingesetzt, um zumindest die Grafiken auf einfachste Art und Weise darstellen zu können. Also so Eye-Candy wie ich sie bereits bei anderen Kernen hatte (Drehen der Spielfigur abhängig vom Fadenkreuz oder Animation beim Laufen) habe ich hier jedoch dann weggelassen.</p> <p>Für die Grafiken greife ich hier pur auf das <a href="proxy.php?url=https://0x72.itch.io/16x16-dungeon-tileset">16x16 Dungeon Tileset von 0x72</a> zu.</p> <p>Ich hab auch überlegt die Grafiken von <a href="proxy.php?url=https://shatteredpixel.com/shatteredpd/">Shattered Pixel Dungeon</a> zu übernehmen, da hier die einzelnen Wände schöner dargestellt sind. Jedoch sind diese auch schwerer zu zeichnen, da es ja einen Unterschied macht, ob man bei einer generierten Dungeon alle Wände gleich anmalt oder Grafik verändern muss, abhängig von Faktoren wie Ausrichtung, Ecken oder Türen.</p> <p>Zudem sind bei den 0x72 Grafiken auch passende Monster dabei. Zwar nicht animiert, aber das ist aktuell auch kein Todo.</p> <p>Um das Hauptprojekt mal abzuschließen, habe ich mich vor ein paar Wochen mal hingesetzt und alle (dringend) notwendigen Features, sowie die damit zusammenhängende Todos priorisiert. Den Rest habe ich für das MVP gestrichen.</p> <p>Weggefallen sind dann so Sachen wie Animationen oder auch der Netzwerkmodus. Eigentlich hätte ich auch noch gerne weitere Items integriert, wie Schlüssel für Türen (oder auch allgemein Türen) oder Schriftrollen mit weiteren Zaubern abseits des Feuerballs. Auch ein paar weitere Gegnerarten wären wünschenswert. Aber primäres Ziel war es dann doch, alles abzuschließen.</p> <p>Übrig geblieben ist dann primär die zufällig generierte Dungeon und die darin enthaltenen Entities (eine Gegnerart, Healthpotions, Ein-/Ausgang und Feuerbälle als Waffe). Zusätzlich noch das voranschreiten in tiefere Ebenen der Dungeon. Beim Beenden der 6. Ebene hat man das Spiel gewonnen, wenn einen die Gegner vorher töten können verloren.</p> <p>Das Endergebnis sieht schon ganz nett aus, ist aber bei weitem nicht der große Wurf, den ich noch zu Beginn erträumt hatte.</p> <p><img alt="Screenshot Gameplay Trailer" class="image-process-image-full" src="proxy.php?url=https://noxzed.de/2021/weird-v1/weird-5.png"></p> <p><a href="proxy.php?url=https://noxzed.de/2021/weird-v1/weird-6.webm">&gt; Gameplay Trailer (1.6 MiB / 2.40min / webm)</a></p> <p>Eigentlich müsste ich das Spiel auch noch paketieren und zum Download anbieten. Mit <a href="proxy.php?url=http://www.pyinstaller.org/">pyinstaller</a> könnte ich jedoch nur Binaries für Linux anbieten und zum Thema Crosscompiling hab ich mir noch keine Gedanken gemacht. Den Source einfach so reinzustellen, bringt vermutlich niemandem etwas, da es bessere Codebeispiele im Netz zu finden gibt.</p> <p>Im Großen und Ganzen habe ich viel bei der Umsetzung des Spiels gelernt. Vorallem in Bezug auf die Einhaltung des Scopes. Ein Spiel, welches man im Kern einige Mal neu schreibt, wird nie fertig. Lieber immer komplett neue Ideen frisch starten und die alten mit der bisherigen Architektur zu Ende bekommen.</p> <p>Jetzt hab ich jedenfalls wieder Lust irgendwas komplett neues anzufangen. Vielleicht mal einen Sidescroller oder einen Raycaster, mal schauen wie ich da die Zeit dafür finde. Vielleicht suche ich mir auch ein Projekt abseits der Spieleentwicklung.</p><ol class="simple-footnotes"><li id="sf-weird-v1-1"><a href="proxy.php?url=https://de.wikipedia.org/wiki/Minimum_Viable_Product">wp:Minimum_Viable_Product</a> <a href="proxy.php?url=#sf-weird-v1-1-back" class="simple-footnote-back">↩︎</a></li><li id="sf-weird-v1-2">Größtenteils große und kleine Quadrate und Kreise. <a href="proxy.php?url=#sf-weird-v1-2-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedMon, 24 May 2021 00:00:00 +0200tag:noxzed.de,2021-05-24:/2021/weird-v1/gamedevgamedevweirdThe idea of Gemmailhttps://noxzed.de/2021/the-idea-of-gemmail/<p>Durch den Unmut mit E-Mail-Anbietern, entstande gestern in #gemini<sup id="sf-the-idea-of-gemmail-1-back"><a href="proxy.php?url=#sf-the-idea-of-gemmail-1" class="simple-footnote" title="irc.tilde.chat unter https://tilde.chat/">1</a></sup> ein interessantes Gespräch über eine Alternative zum bestehenden E-Mail-System, welches die Prinzipien von <a href="proxy.php?url=https://gemini.circumlunar.space/">Gemini</a> umsetzt.</p> <p>Grundgedanke war es, die bestehenden E-Mail-Protokolle (SMTP, IMAP, MAIL) mit neuen Protokollen für s2s, c2s und E-Mail-Envelope zu ersetzen, um bei der Gelegenheit die ganzen Legacysachen aus den Protokollen zu entfernen und diese von Anfang an Leichtgewichtig und vollständig verschlüsselt zu designen. Idealerweise auch möglichst SPAM-sicher (sonst wäre es ja kaum ein Fortschritt).</p> <p>Hier ein paar der Gedanken unzusammenhängend aufgeschrieben:</p> <ul> <li>UTF8</li> <li>TLS</li> <li>TOFU: Server tauschen durch ersten Kontakt Security Daten aus</li> <li>gemtext als E-Mail Body nutzen</li> <li>Text und Anhang der Mail sind vollständig asynchron verschlüsselt</li> <li>Envelope wird durch den Senderserver signiert</li> <li>»Passer«-Server, welche Mails weiterleiten (der Quellserver muss diesen das jedoch mit einem Schlüssel erlauben, den der Zielserver prüfen kann)</li> <li>Ein Ziel- oder Quellserver kann manchmal nicht erreichbar sein (zB. wenn es ein solarbetriebener Homeserver ist)</li> <li>Das s2s-Protokoll sollte weiterhin ein Pull-Protokoll bleiben</li> <li>Attachments werden (optional) nicht mit in der Mail versendet, sondern lediglich mit einem Link (gemini, http, ipfs) referenziert, sowie einem Key zur Entschlüsselung und ein Ablaufdatum. Damit müsste keine riesige Mail versendet werden, welche ggf. vom Empfängerserver abgelehnt werden müsste.</li> <li>Dem Serverbetreiber sollten so wenig Informationen wie möglich zugänglich gemacht werden</li> </ul> <p>Mir kommen bei der ganzen Thematik gerade einige Parallelen zu Messengern in den Sinn. Vermutlich gäbe es hier durchaus einige Schnittpunkte. Das <a href="proxy.php?url=https://de.wikipedia.org/wiki/Signal-Protokoll">Axolotl-Protokoll</a> zum Verschlüsseln der Nachrichten und die Möglichkeit mehrere Personen gleichzeitig anzuschreiben (Gruppen) wären natürlich schon ein guter Mehrwert.</p> <p>Die Verwendung von eigenen Domains sollte genauso möglich sein, wie das Migrieren der eigenen Daten auf einen anderen Server. Mit IMAP ist sowas bei Mails ja relativ einfach möglich: Mails alle runterladen, Mails beim anderen Anbieter alle wieder hochladen.</p> <p>Möglicher Aufbau eines »Gemmail-Postfaches« könnte zB so aussehen:</p> <ul> <li>INBOX: Server legt neue Mails in diesem Ordner an</li> <li>OUTBOX: Inhalte werden vom Server versendet und dann gelöscht</li> <li>DATA: enthält eine (mehrere?) verschlüsselte .tar Archive, welche die verschlüsselten Mails enthalten. Diese Archive werden von den Clients heruntergeladen und verschlüsselt auch wieder hochgeladen.</li> </ul> <p>Ziel wäre es, dass der Server so wenig Metadaten wie möglich bekommt.</p> <p>Ich bin mal gespannt, ob sich aus der Idee in den nächsten Monaten etwas ergeben sollte.</p><ol class="simple-footnotes"><li id="sf-the-idea-of-gemmail-1">irc.tilde.chat unter <a href="proxy.php?url=https://tilde.chat/">https://tilde.chat/</a> <a href="proxy.php?url=#sf-the-idea-of-gemmail-1-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedMon, 26 Apr 2021 00:00:00 +0200tag:noxzed.de,2021-04-26:/2021/the-idea-of-gemmail/smallwebsmallwebgeminiRelease: Webferea 2.2.0https://noxzed.de/2021/release-webferea-220/<p>In den letzten Wochen sind mir beim Nutzen von Webferea einige Sachen aufgefallen, die ich noch korrigieren bzw. implementieren musste.</p> <p>Beim Rendern von Feedeinträgen, welche iframes enthielten (gerade YT) wurde die Ausgabe des Posts dadurch unterbrochen. Das Verhalten konnte ich browserübergreifend beobachten. Um das zu korrigeren, wird nun vor der Ausgabe des Posts, der iframe-Tag entsprechend herausgefiltert und lediglich ein Link auf das src-Attribute erstellt.</p> <p>Neben dem iframe-Tag filtere ich nun auch script-Tags raus, deren src außerhalb der Feed-Domain liegt und Links/Bilder Referenzen werden nun auch relativ zur Feed-Domain konvertiert.</p> <p>Liferea bietet das Feature, dass der Inhalt eines Posts nochmal vollständig von der Webseite heruntergeladen werden kann, falls der Feed nur eine Zusammenfassung enthält. Die berücksichtige ich nun auch bei der Ausgabe des vollständigen Feeds.</p> <p>Den Sync zwischen Client und Server wurde durch das mit bz2 komprimierte Übertragen der sqlite-Datenbank beschleunigt. Die Datei wird dadurch auf ca. 20% ihrer eigentlichen Größe verkleinert. Gerade der Upload kann durch separater Komprimierung schneller durchgeführt werden.</p> <p>Sonst sich noch viele weitere Fixes eingeflossen, welche mir beim täglichen Benutzen aufgefallen sind.</p> <p>Zu finden ist das Projekt aktuell auf <a href="proxy.php?url=https://github.com/CydNoxzed/webferea2">Github</a>.</p>Cyd NoxzedSun, 18 Apr 2021 00:00:00 +0200tag:noxzed.de,2021-04-18:/2021/release-webferea-220/projectsprojectswebfereaSubscribe and Commenthttps://noxzed.de/2021/subscribe-and-comment/<h2>RSS Feed</h2> <p>In der letzten Woche habe ich auf dem Blog die Links zu den Feeds geändert. Bei der Gelegenheit lasse ich auch die Generierung der ATOM-Feeds bleiben und stelle alle Feeds nur noch im RSS-Format bereit.</p> <p>Für meine Anforderungen reichen beide Formate, da sich diese bei den Features nur im Detail unterscheiden. Jeder Feedreader kann sowieso beide Formate lesen, somit macht es keinen Unterschied auf welches Format ich mich konzentriere. Ich habe mich jetzt halt für RSS entschieden, weil das Format mir irgendwie sympatischer ist<sup id="sf-subscribe-and-comment-1-back"><a href="proxy.php?url=#sf-subscribe-and-comment-1" class="simple-footnote" title="Eigentlich ein echt schlechter Grund eine Format-Entscheidung zu treffen. Ich bin also offen für richtige Argumente.">1</a></sup>.</p> <p>Der Hauptfeed ist nun unter <a href="proxy.php?url=https://noxzed.de/feed/index.xml">https://noxzed.de/feed/</a> zu erreichen und die jeweiligen Tag-Feeds unter der jeweiligen Übersichtsseite des Tags, zB. <a href="proxy.php?url=https://noxzed.de/tag/projects/feed/">https://noxzed.de/tag/projects/feed/</a>.</p> <h2>Kommentieren</h2> <p>Mit RSS gibts einen einfachen Weg, Inhalte zu abonnieren und auf neue Posts aufmerksam gemacht zu werden (idR durch den Feedreader). Jedoch ist das einen pure Einbahnsstrasse. Es gibt damit leider keinen Weg auf Beiträge zu kommentieren und dem Ersteller auf diese Kommentare hinzuweisen.</p> <p>HTTP ist primär ein <a href="proxy.php?url=https://de.wikipedia.org/wiki/Pull-Medien">Pull-Protokoll</a> und es ist deshalb nicht so einfach möglich einen Kommentar an den Ersteller zu versenden<sup id="sf-subscribe-and-comment-2-back"><a href="proxy.php?url=#sf-subscribe-and-comment-2" class="simple-footnote" title="Das Erstellen eines Kommentares mit fest definierten Feldern und einem POST-Request könnte eigentlich relativ einfach umgesetzt werden könnte. Wobei hier auch Spam Tür und Tor geöffnet werden.">2</a></sup>. Normalerweise ist direkt am Ende eines Posts ein Kommentarformular eingebunden und kann verwendet werden, auch gibt es Techniken wie <a href="proxy.php?url=https://de.wikipedia.org/wiki/Trackback">Trackback</a>, <a href="proxy.php?url=https://de.wikipedia.org/wiki/Pingback">Pingback</a> oder <a href="proxy.php?url=https://indieweb.org/Webmention">Webmention</a> die ein Kommentar auf einer anderen Seite Referenzieren können.</p> <p>Was verwendet man hier nun? Mit Web-Formularen zu hantieren, ergibt wenig Sinn, wenn der Blog selbst eine statische Seite ist. Wobei es auch hier durchaus <a href="proxy.php?url=https://github.com/knowbl/log-based-static-contact-form">Möglichkeiten</a> gibt das alles abzufangen.</p> <p>Um alles möglichst einfach und simple zu halten, biete ich unterhalb jedes Posts ab jetzt ledliglich einen <a href="proxy.php?url=https://de.wikipedia.org/wiki/Mailto">mailto-Link</a> an, in welchem der Betreff bereits mit dem Titel des Posts vorgefüllt wird. Um Crawlern nach Mail-Adressen die Arbeit ein bisschen zu erschweren, ist der Link mit <a href="proxy.php?url=https://de.wikipedia.org/wiki/Entit%C3%A4t_(Auszeichnungssprache)">HTML-Entities</a> encodiert<sup id="sf-subscribe-and-comment-3-back"><a href="proxy.php?url=#sf-subscribe-and-comment-3" class="simple-footnote" title="Ich hab keine Ahnung, ob das wirklich was bringt, aber ich würde es jetzt einfach mal testen. Mal schauen, ob überhaupt Spam bis zu mir vordringt, mein Mailprovider filtert ja sowieso den Posteingang.">3</a></sup>.</p><ol class="simple-footnotes"><li id="sf-subscribe-and-comment-1">Eigentlich ein echt schlechter Grund eine Format-Entscheidung zu treffen. Ich bin also offen für richtige Argumente. <a href="proxy.php?url=#sf-subscribe-and-comment-1-back" class="simple-footnote-back">↩︎</a></li><li id="sf-subscribe-and-comment-2">Das Erstellen eines Kommentares mit fest definierten Feldern und einem POST-Request könnte eigentlich relativ einfach umgesetzt werden könnte. Wobei hier auch Spam Tür und Tor geöffnet werden. <a href="proxy.php?url=#sf-subscribe-and-comment-2-back" class="simple-footnote-back">↩︎</a></li><li id="sf-subscribe-and-comment-3">Ich hab keine Ahnung, ob das wirklich was bringt, aber ich würde es jetzt einfach mal testen. Mal schauen, ob überhaupt Spam bis zu mir vordringt, mein Mailprovider filtert ja sowieso den Posteingang. <a href="proxy.php?url=#sf-subscribe-and-comment-3-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedSat, 17 Apr 2021 00:00:00 +0200tag:noxzed.de,2021-04-17:/2021/subscribe-and-comment/projectsblogprojectsDer Gegenpol zum Commercial Web: Das Small Webhttps://noxzed.de/2021/the-small-web/<p>Als Gegenpol des »Commercial Web« haben sich in den letzten Jahren einige Alternativen herausgebildet, welchen man durchaus unter dem Begriff <a href="proxy.php?url=https://neustadt.fr/essays/the-small-web/">"Small Web"</a> zusammenfassen kann.</p> <p>Allen gemein ist, dass sie sich auf dezentralen Beinen gegen die aktuell vorherschenden Plattformen stellen und eine transparente und unabhängigere Alternative zum Austausch mit Gleichgesinnten bieten.</p> <p>Besonders ins Auge gefallen sind mir hierbei:</p> <ul> <li><a href="proxy.php?url=https://indieweb.org/">Indieweb</a></li> <li><a href="proxy.php?url=https://activitypub.rocks/">ActivityPub</a></li> <li><a href="proxy.php?url=https://tildeverse.org/">Tildeverse</a></li> </ul> <p>Beim Indieweb liegen die Daten bei den Benutzern selbst auf den eigenen Servern, welche mit einer persönlichen Domain aufgerufen werden. Verbunden werden die einzelnen Inhalte durch Metadaten im HTML (<a href="proxy.php?url=http://microformats.org/">Microformats</a>) und <a href="proxy.php?url=https://indieweb.org/Webmention">Webmention</a>.</p> <p>ActivityPub ist der Versuch ein allumfassendes Soziales Netzwerk aufzubauen, welches die aktuellen Größen wie Facebook oder Twitter ersetzen soll. Obwohl die Grundlage durch die Benutzung von <a href="proxy.php?url=https://de.wikipedia.org/wiki/Activity_Streams_(format)">ActivityStreams</a> ziemlich durchdacht und logisch wirkt, scheint es wohl <a href="proxy.php?url=https://overengineer.dev/blog/2019/01/13/activitypub-final-thoughts-one-year-later.html">langfristig stärkere Hürden</a> bei der Implementierung zu geben. Einen ActivityPub-Client hab ich bisher gar nicht gefunden, nur Mastodon kompatible Clients.</p> <p>Die Ableger des <a href="proxy.php?url=https://medium.com/message/tilde-club-i-had-a-couple-drinks-and-woke-up-with-1-000-nerds-a8904f0a2ebf">tilde.club</a> folgen der Tradition der alten Unix-Shared-Hostings und sind allgemein sehr Retro angelegt (zB wird stark auf <a href="proxy.php?url=https://gemini.circumlunar.space/">Gemini</a> gesetzt) und auch eher was für Technerds.</p> <p>Und das gibt es noch <a href="proxy.php?url=https://solidproject.org/">Solid</a>. Ich habe aber bisher nicht ganz verstanden wie das genau läuft, jedoch finde ich die Möglichkeit im Besitz der eigenen Daten zu bleiben ziemlich interessant<sup id="sf-the-small-web-1-back"><a href="proxy.php?url=#sf-the-small-web-1" class="simple-footnote" title="Wobei im Internet grundsätzlich alle Daten bei der Übertragung kopiert werden und man strenggenommen nicht der Besitzer der Daten dauerhaft bleibt.">1</a></sup>.</p> <p>Hierzu noch ein paar weiterführende Links:</p> <ul> <li><a href="proxy.php?url=https://overengineer.dev/blog/2020/01/01/2010s-alternative-social-media.html" title="A decade full of work, hope, and disappointment">The 2010s and alternative Social Media</a></li> <li><a href="proxy.php?url=https://drewdevault.com/2020/09/20/The-potential-of-federation.html">The unrealized potential of federation</a></li> <li><a href="proxy.php?url=https://runyourown.social/" title="How to run a small social network site for your friends">Run your own social</a></li> <li><a href="proxy.php?url=https://seirdy.one/2021/02/23/keeping-platforms-open.html">Keeping platforms open</a></li> </ul><ol class="simple-footnotes"><li id="sf-the-small-web-1">Wobei im Internet grundsätzlich alle Daten bei der Übertragung kopiert werden und man strenggenommen nicht der Besitzer der Daten dauerhaft bleibt. <a href="proxy.php?url=#sf-the-small-web-1-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedSun, 04 Apr 2021 00:00:00 +0200tag:noxzed.de,2021-04-04:/2021/the-small-web/smallwebsmallwebWebferea 2https://noxzed.de/2021/webferea-2/<p>Letztes Wochenende habe ich die freie Zeit genutzt, um einige Features bei Webferea zu implementieren und die ganze Codebasis auf einen schöneren Stand zu bringen.</p> <p>Dabei ist eher eine neue Major-Version herausgekommen, da ich durch die langsam notwendige Modularisierung der Codebasis lieber gleich komplett neu angefangen habe.</p> <p>Herausgekommen ist <a href="proxy.php?url=https://github.com/CydNoxzed/webferea2">Webferea2</a>.</p> <p>Darin enthalten sind auch neue Features, wie:</p> <ul> <li>syncen der liferea.db über https anstelle sftp</li> <li>angenehmeres Layout/Design, welches auf <a href="proxy.php?url=https://watercss.kognise.dev/">water.css</a> aufbaut</li> <li>Posts ohne separaten Seitenreload als gelesen markieren und zur Liste zurückkehren</li> <li>Markieren von Posts auf der Liste</li> <li>eingebettete Youtube-Links/Iframes werden jetzt gefiltert und zerstören somit nicht mehr das HTML-Rendering</li> <li>relative Bildlinks innerhalb von Posts werden zu absoluten Links umgewandelt</li> </ul> <p>Hier sind die alte und neue Version im direkten Vergleich:</p> <p><a href="proxy.php?url=/2021/webferea-2/webferea-v1.png"><img alt="Webferea v1" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2021/webferea-2/webferea-v1.png"></a> <a href="proxy.php?url=/2021/webferea-2/webferea-v2.png"><img alt="Webferea v2" class="image-process-image-half" src="proxy.php?url=https://noxzed.de/2021/webferea-2/webferea-v2.png"></a></p> <p>Geplant wäre jetzt noch eine "Bewertungs"-Funktion für einzelne Posts, damit ich langfristige Statistiken sammeln kann, ob sich das Abo eines Feeds lohnt oder nicht. Bin mir aber noch nicht sicher, wie ich die einzelnen Posts bewerten soll, damit ich am Ende auch zuverlässige Aussagen über einen Feed treffen kann. Eine einfache +1/-1 Bewertung reicht vermutlich nicht, ob einen Feed umfangreich kuratieren zu können.</p>Cyd NoxzedFri, 02 Apr 2021 00:00:00 +0200tag:noxzed.de,2021-04-02:/2021/webferea-2/projectspythonflaskrssprojectswebfereaMessenger 2021 - Anwärterhttps://noxzed.de/2021/messenger-2021-anwarter/<p>Dies ist ein Fortsetzungsartikel zu <a href="proxy.php?url=/2021/messenger-2021-anforderungen/">Messenger 2021 - Anforderungen</a>.</p> <h2>Matrix und XMPP</h2> <p>Nach längerem Einlesen in diversen Posts und Artikeln waren mir die Favoriten nach kurzer Zeit klar: XMPP und Matrix.</p> <p>Beide verfolgen leicht andere Ziele und unterscheiden sich in diesen Punkten auch entsprechend von technischer Seite voneinander. Während XMPP durchaus in der Tradition der Messenger aus den 2000ern steht, ist Matrix ein Kind der 2010er, welches das modernere Slack als Vorbild hat. Beide stehen sich jedoch in nichts nach, werden aktuell weiterentwickelt und haben jeweils eigene <a href="proxy.php?url=https://wiki.404.city/en/XMPP_vs_Matrix">Vor- und Nachteile</a><sup id="sf-messenger-2021-anwarter-1-back"><a href="proxy.php?url=#sf-messenger-2021-anwarter-1" class="simple-footnote" title="404.city betreibt einen eigenen XMPP-Server.">1</a></sup>.</p> <p>Auch ein Nachrichtenaustausch zwischen den beiden kann via <a href="proxy.php?url=https://matrix.org/docs/projects/as/matrix-xmpp-bridge">Brücken</a> realisiert werden.</p> <p>Das Problem ist jedoch: Man bekommt das nicht technikafinen Menschen nicht leicht erklärt. Gerade, wenn diese aus der WhatsApp-Sphäre kommen.</p> <p>Einen Account anlegen und alle Kontakte neu suchen?<sup id="sf-messenger-2021-anwarter-2-back"><a href="proxy.php?url=#sf-messenger-2021-anwarter-2" class="simple-footnote" title="Quicksy versucht dieses Problem zu beheben, würde aber auch nur funktionieren, wenn die anderen Kontakte auch Quicksy nutzen. Matrix nutzt ihren zentralen Discovery-Server, bei dem man optional seine Handynummer mit seiner Matrix-ID verknüpfen kann.">2</a></sup> Welchen Server soll ich verwenden? Welchen Client soll ich installieren und warum gibts so viele? Kann ich damit Feature-XY nutzen?</p> <h2>Signal</h2> <p>Signal ist hierfür die Antwort. Es funktioniert ähnlich genug wie die anderen großen Messenger, sodass die Bedienung für normale Benutzer keine große Hürde darstellt. Dabei unterscheidet es sich von anderen Markteilnehmern dadurch, dass nicht nur der Client quelloffen ist, sondern auch der Server. Somit hätte man (zumindest theoretisch) die Möglichkeit, das ganze Netzwerk zu forken, wenn die Macher hinter Signal einen anderen Weg einschlagen. Die <a href="proxy.php?url=https://drewdevault.com/2018/08/08/Signal.html">Nachteile</a>, die dabei gerne aufgezählt werden, lassen sich anhand der gewählten <a href="proxy.php?url=https://signal.org/blog/the-ecosystem-is-moving/">Strategie</a> von Signal durchaus nachvollziehen. Volle Kontrolle über Client und Server zu haben, erlaubt es einen durchaus, schnell Änderungen und Fixes einzuspielen. Wobei man dies auch mit einem soliden Framework oder Library abdecken könnte, welche alle unterschiedlichen Clients und Server nutzen. Die Bindung an die Telefonnummer, wird dafür benutzt, die bereits aufgebaute Kontaktliste auf dem Handy mitzuverwenden, ohne das man hier nochmal extra Personen irgendwo hinzufügen muss.</p> <h2>Wer ist nun der Anwärter?</h2> <p>Langfristig bleibt zu hoffen, dass Signal vielleicht doch Föderation aktiviert und es somit auch XMPP, Matrix und IRC Brücken geben kann. Ein weiterer Signal Client für Handy und Desktop wären natürlich auch nicht schlecht.</p> <p>Vielleicht braucht es auch nur ein neues einfaches Protokoll für genau diesen Anwendungszweck. Ähnlich wie es HTTP für das Web war, benötigen wir so ein simples und universelles Protokoll vielleicht nur für Chats. Wobei das woran ich hier denke vermutlich einfach nur die E-Mail ist.</p> <p>Es ist schon irgendwie tragisch, dass die Netzwerke untereinander nicht operabel sind. Letzten Endes werden auch nur ein paar Texte, Bilder und Metadaten hin und her geschickt.</p> <p>Eigentlich fasst es die Republik mit <a href="proxy.php?url=https://www.republik.ch/2021/02/24/kill-the-messenger">Kill The Messenger</a> ganz gut zusammen. Auch wenn sich dadurch vermutlich einige neue Probleme auftun.</p> <p><strong>Update:</strong></p> <p>Der Code der Serversoftware wurde von Signal wohl schon länger nicht mehr auf Github gepushed.<sup id="sf-messenger-2021-anwarter-3-back"><a href="proxy.php?url=#sf-messenger-2021-anwarter-3" class="simple-footnote" title="linuxnews.de: Server-Code des Signal-Messenger auf GitHub veraltet">3</a></sup> Damit fällt ein wichtiger Pluspunkt weg, weswegen mal Signal als Messenger der Wahl auf seinem Smartphone installieren sollte.</p> <p>Vielleicht wird der Code ja in den nächsten Wochen mal gepushed oder man bekommt ein offizielles Statement, warum der Code veraltet ist.</p><ol class="simple-footnotes"><li id="sf-messenger-2021-anwarter-1"><a href="proxy.php?url=https://404.city">404.city</a> betreibt einen eigenen XMPP-Server. <a href="proxy.php?url=#sf-messenger-2021-anwarter-1-back" class="simple-footnote-back">↩︎</a></li><li id="sf-messenger-2021-anwarter-2"><a href="proxy.php?url=https://quicksy.im/">Quicksy</a> versucht dieses Problem zu beheben, würde aber auch nur funktionieren, wenn die anderen Kontakte auch Quicksy nutzen. Matrix nutzt ihren zentralen Discovery-Server, bei dem man optional seine Handynummer mit seiner Matrix-ID verknüpfen kann. <a href="proxy.php?url=#sf-messenger-2021-anwarter-2-back" class="simple-footnote-back">↩︎</a></li><li id="sf-messenger-2021-anwarter-3"><a href="proxy.php?url=https://linuxnews.de/2021/03/server-code-des-signal-messenger-auf-github-veraltet/">linuxnews.de: Server-Code des Signal-Messenger auf GitHub veraltet</a> <a href="proxy.php?url=#sf-messenger-2021-anwarter-3-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedSun, 14 Mar 2021 00:00:00 +0100tag:noxzed.de,2021-03-14:/2021/messenger-2021-anwarter/miscmiscmessengerMessenger 2021 - Anforderungenhttps://noxzed.de/2021/messenger-2021-anforderungen/<p>Die Anforderungen an einen Messenger haben sich aus meiner Sicht in den letzten 5 Jahren nicht groß verändert:</p> <ul> <li>Verschlüsselung</li> <li>Förderiert</li> <li>Freie Software</li> <li>Gruppenfähig</li> <li>Mobil nutzbar</li> <li>Multi-Client</li> <li>Von Handynummer unabhängiger Identifier</li> </ul> <p>Die gängigen Messenger<sup id="sf-messenger-2021-anforderungen-1-back"><a href="proxy.php?url=#sf-messenger-2021-anforderungen-1" class="simple-footnote" title="WhatsApp, Telegram, Signal, Threema, etc">1</a></sup> erfüllen diese <a href="proxy.php?url=https://infosec-handbook.eu/blog/discussion-secure/#sm">Anforderungen</a> recht unterschiedlich. <a href="proxy.php?url=https://www.eff.org/pages/secure-messaging-scorecard">Übersicht</a> <a href="proxy.php?url=https://de.wikipedia.org/wiki/Liste_von_Instant-Messaging-Protokollen">bieten</a> <a href="proxy.php?url=https://www.securemessagingapps.com/">diverse</a> <a href="proxy.php?url=https://media.kuketz.de/blog/messenger-matrix/messenger-matrix.html">Vergleichsmatrizen</a>.</p> <p>Bei den FLOSS Messengern gibt es die drei Platzhirsche <a href="proxy.php?url=https://de.wikipedia.org/wiki/Internet_Relay_Chat">IRC</a>, <a href="proxy.php?url=https://xmpp.org/">XMPP</a> und <a href="proxy.php?url=https://matrix.org/">Matrix</a>.</p> <h2>Verschlüsselung</h2> <p>Ende-zu-Ende Verschlüsselung ist obligatorisch und muss per default aktiviert sein. Nach den <a href="proxy.php?url=https://de.wikipedia.org/wiki/Globale_%C3%9Cberwachungs-_und_Spionageaff%C3%A4re">Ereignissen um Snowden</a> wäre alles andere ziemlich fahrlässig. Bei Postkarten will man ja auch nicht, dass diese von allen Behörden und der Post selbst gelesen werden können (und digital auch werden). </p> <h2>Förderiert</h2> <p>Um einen Messenger möglichst unabhängig zu haben, sollte es möglich sein einen eigenen Server aufzusetzen oder auch einfach den Homeserver zu wechseln, ohne dabei auch alle Kontakte zu verlieren.</p> <p>Vorbild für einen föderierten Nachrichtenservice ist hier sicherlich E-Mail. Bei Messengern ist es allgemein akzeptiert, dass man als Threema-Nutzer keine Nachrichten an Whatsapp- oder Telegram-Nutzer senden kann. Bei E-Mail wäre aber vermutlich das unverständnis erstmal groß, warum man keine E-Mail von GMX.de nicht zu GMAIL.com senden kann. </p> <p>Die E-Mail Infrastruktur ist hier aber leider auch nicht perfekt. Deutschlandweit konzentrieren sich die Postfächer bei einigen <a href="proxy.php?url=https://www.statista.com/statistics/446418/most-popular-e-mail-providers-germany/">großen Providern</a>. <sup id="sf-messenger-2021-anforderungen-2-back"><a href="proxy.php?url=#sf-messenger-2021-anforderungen-2" class="simple-footnote" title="Die größten deutschen Anbieter finden sich alle unter dem Dach von United Internet.">2</a></sup></p> <h2>Freie Software</h2> <p>Client und Server sollten selbst kompilierbar sein. Idealerweise gibt es dementsprechend auch unterschiedliche Implementationen und nicht nur eine.</p> <p>Wenn der Code nicht öffentlich ist, spielt es auch keine Rolle ob der Messenger die Nachrichten verschlüsselt überträgt. Man kann nicht prüfen, ob die Nachricht nicht auch noch woanders hin übertragen worden ist.</p> <p>Damit man überprüfen kann, ob das Programm, das gerade läuft auch aus dem veröffentlichen Code enstanden ist, können <a href="proxy.php?url=https://en.wikipedia.org/wiki/Reproducible_builds">Reproducible builds</a> helfen.</p> <h2>Gruppenfähig</h2> <p>Bei Gruppen spielen die Messenger ihre große Stärke aus und erzwingen hier auch die notwendigen Netzwerkeffekte. Einer Einzelperson kann man auch eine SMS oder MMS<sup id="sf-messenger-2021-anforderungen-3-back"><a href="proxy.php?url=#sf-messenger-2021-anforderungen-3" class="simple-footnote" title="Ich kenne ehrlich gesagt keinen, der mehr als eine Handvoll MMS verschickt und empfangen hat.">3</a></sup> zuschicken. Beim Koordinieren von einer ganzen Gruppe an Personen ist die Möglichkeit an ein ganzes Set an Personen gleichzeitig zu schreiben eine enorme Erleichterung, gerade wenn die Leute auch an das gleiche Set ohne Probleme antworten können.</p> <h2>Mobil nutzbar</h2> <p>Soweit ich das sagen kann, findet die Kommunikation dieser Jahre primär über das Smartphone statt. Primär hier vor allem über die diversen Messenger.</p> <p>Ein Messenger, muss hier zwangsläufig auch die mobilen Endgeräte und deren plötzlichen Online/Offline-Wechsel mitmachen. </p> <p>Die dafür ausgelegten bekannten Messenger haben damit keine Probleme. Die FLOSS-Alternativen, welche teilweise noch aus der Zeit vor dem Smartphone kommen, jedoch schon.</p> <p>Wenn man im IRC offline ist, dann ist man auch nichtmehr in den entsprechenden Gruppen (Channels) präsent und würde dort auch alle Nachrichten verpassen. Abhilfe würde hier erst ein <a href="proxy.php?url=https://en.wikipedia.org/wiki/Bouncer">Bouncer</a> schaffen.</p> <p>Bei XMPP hat die Unterstützung dafür ziemlich auf sich warten lassen. Doch mittlerweile sind einige <a href="proxy.php?url=https://xmpp.org/extensions/xep-0286.html#xeps">XEPs</a> speziell hierfür verfügbar und in den Clients integriert. Das sah 2014 noch <a href="proxy.php?url=https://op-co.de/blog/posts/mobile_xmpp_in_2014/">ganz anders</a> aus.</p> <h2>Verbindung mit mehreren Clients parallel</h2> <p>XMPP hat dies schön mit den <a href="proxy.php?url=https://de.wikipedia.org/wiki/Jabber_Identifier#Ressource">Ressourcen</a> gelöst. Diese Herangehensweise ist für den Durchschnittsbenutzer vermutlich zu kompliziert, könnte ihm aber durchaus auch vollkommen verborgen bleiben.</p> <p>Die aktuellen mobilen Messenger haben sich mittlerweile Desktop-Apps etabliert. Diese sind - zwar dem Stand der Technik geschuldet - auf <a href="proxy.php?url=https://de.wikipedia.org/wiki/Electron_(Framework)">Electron</a> basierende abgespeckte Browser, aber immerhin.</p> <h2>Von Handynummer unabhängiger Identifier</h2> <p>Bei vielen Plattformen wird mittlerweile eine <a href="proxy.php?url=https://de.wikipedia.org/wiki/Zwei-Faktor-Authentisierung">2FA</a> mit einer gültigen Handynummer erzwungen. Hintergedanke ist vermutlich, dass hiermit eine bessere Identifikation des Benutzers möglich ist. E-Mailadressen kann man durchaus unendlich haben, bei Handynummern vermutlich selten mehr als zwei.</p> <p>Eine Identifikation der dahinterstehenden Person wäre damit immernoch möglich. Mal davon abgesehen, das man mit seiner Handynummer auch unabhängig von einem Messenger via SMS oder telefonisch kontaktiert werden kann. Das muss nicht zwangsweise ein Nachteil sein.</p><ol class="simple-footnotes"><li id="sf-messenger-2021-anforderungen-1">WhatsApp, Telegram, Signal, Threema, etc <a href="proxy.php?url=#sf-messenger-2021-anforderungen-1-back" class="simple-footnote-back">↩︎</a></li><li id="sf-messenger-2021-anforderungen-2">Die größten deutschen Anbieter finden sich alle unter dem Dach von <a href="proxy.php?url=https://de.wikipedia.org/wiki/United_Internet">United Internet</a>. <a href="proxy.php?url=#sf-messenger-2021-anforderungen-2-back" class="simple-footnote-back">↩︎</a></li><li id="sf-messenger-2021-anforderungen-3">Ich kenne ehrlich gesagt keinen, der mehr als eine Handvoll MMS verschickt und empfangen hat. <a href="proxy.php?url=#sf-messenger-2021-anforderungen-3-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedMon, 08 Mar 2021 00:00:00 +0100tag:noxzed.de,2021-03-08:/2021/messenger-2021-anforderungen/miscmiscmessengerLiferea mobil nutzenhttps://noxzed.de/2021/liferea-mobil-nutzen/<p>Schon seit über 10 Jahren nutze ich zum Lesen von RSS-Feeds<sup id="sf-liferea-mobil-nutzen-1-back"><a href="proxy.php?url=#sf-liferea-mobil-nutzen-1" class="simple-footnote" title="Damit meine ich auch Atom- oder andere Web-Feeds. Das dass von technischer Seite nicht ganz passt, ist mir bewusst.">1</a></sup> <a href="proxy.php?url=https://lzone.de/liferea/">Liferea</a>.</p> <p>Damals bestand mein Internetkontingent mangels DSL aus ungefähr 1500 MiB UMTS pro Monat<sup id="sf-liferea-mobil-nutzen-2-back"><a href="proxy.php?url=#sf-liferea-mobil-nutzen-2" class="simple-footnote" title="Heutzutage übertrage ich schonmal 80 - 160 GiB pro Monat, auf meinem Desktoprechner allein. Der Großteil wird hierbei sicherlich aus Videos und Cloudanbindung bestehen. Sollte ich bei Gelegenheit vielleicht mal auswerten.">2</a></sup> (Down- und Upload). Jedes Megabyte war da kostbar und ich habe mir da verschiedene Strategien überlegt, um möglichst viel Traffic zu sparen. Angefangen von extremen Caching über einen <a href="proxy.php?url=http://www.squid-cache.org/">lokalen Proxy</a>, bis hin zum Blocken von Bildern auf Webseiten und deren Nachladen erst auf Klick/Anfrage.</p> <p>Gerade beim täglichen Abklappern der favorisierten Webseiten, bin ich früher oder später dann auf RSS gestoßen.</p> <p>Erstmal hab ich in meinem damaligen Mailclient<sup id="sf-liferea-mobil-nutzen-3-back"><a href="proxy.php?url=#sf-liferea-mobil-nutzen-3" class="simple-footnote" title="Vermutlich Thunderbird, wenn mich meine Erinnerungen nicht täuschen.">3</a></sup> einige Feeds abonniert, aber so ganz praktikable war das Abrufen der Feeds im Mailclient nicht. Hat alles wunderbar funktioniert, aber gedanklich war es eben ein Programm für Mails und nicht für Feeds. Also ging ich auf die Suche nach einem Stand-Alone Programm. Als Gnome/GTK-User hab ich Liferea ausprobiert und bin dabei dann erstmal auch geblieben.</p> <p>Irgendwann relativ spät, verspürte ich auch den Drang, meine ungelesenen Feeds am Smartphone weglesen zu können. Wichtig war mir hier eigentlich nur, dass ich keine separate Feedliste pflegen musste und die Flags (Gelesen und Markiert) an den Items jeweils synchronisiert werden. Die leichteste Variante wäre es vermutlich gewesen einfach selbst einen Feedreader auf meinem Server zu hosten und die bisherigen Liferea-Feeds dorthin zu synchronisieren. Am Rechner bevorzuge ich jedoch native Clients, weil ich nicht alles im Browser nutzen will (Electron-based Apps fallen da auch mit rein).</p> <p>Auf dem Smartphone ist die Sachlage leider ein bisschen anders. Apps fokusieren sich meistens auf einen Cloud-Ansatz und sind oftmals auch nicht self-hosted nutzbar. Leider bin ich jetzt kein App-Entwickler, zumindest habe ich mich damit noch nie groß befasst. Die einfachste Lösung war es fürs Smartphone eine Webseite zu bauen, welche die Feeds lediglich auflistet und die Items öffnen/lesen lässt. Da Liferea, alle Feeds in einer SQLite-Datenbank verwaltet und ich mich sowieso mal mit Webprogrammierung in Python auseinandersetzen wollte, hab ich mich kurzerhand mal in <a href="proxy.php?url=https://palletsprojects.com/p/flask/">Flask</a> eingelesen.</p> <p>Eine Loginseite war dank des Tutorial schnell geschrieben und auch eine Listen-, sowie eine Artikelansicht selbst war auch schnell als Template anglegt und gefüllt. Auf den Artikelseiten dann noch Buttons zum Flaggen von »Gelesen« und »Markieren« hinzugefügt und diese Infos dann wieder in die SQLite-Datenbank abpeichern und fertig.</p> <p>In der letzten Dekade haben sich bei mir mittlerweile 150 Feeds angesammelt <sup id="sf-liferea-mobil-nutzen-4-back"><a href="proxy.php?url=#sf-liferea-mobil-nutzen-4" class="simple-footnote" title="Von den 150 Feeds sind fast die Hälfte Feeds von Audio-Podcasts und YT-Videos.">4</a></sup>, wobei diese auch immer schon fluktuieren. Manchmal kommt was Neues hinzu und manchmal fliegen Feeds wieder aus dem Reader raus, je nach aktueller Interessenlage. Auf dem Smartphone will ich jedoch nur eine Auswahl an Feeds lesen (primär kurze Artikel, die man leicht zwischendrinn lesen kann). Also filtere ich die Liste der anzuzeigenen Feeds, um nur Artikel von ausgewählten Feeds angezeigt zu bekommen.</p> <p>Um jetzt, die beiden SQLite-Datenbanken wieder in Einklang zu bringen führt ein Synchronisationsskript folgendes aus:</p> <ol> <li>Download der SQLite-Datenbank vom Server</li> <li>Übernahme aller veränderten Flags (gelesen, markiert) in die Datenbank des Desktoprechners</li> <li>Upload der aktualisierten lokalen Datenbank auf den Server</li> </ol> <p>Upload und Download funktioniert aktuell via SFTP, schöner wäre hier vermutlich eine Übertragung via HTTPS, für das Feature hatte ich jedoch noch keine Zeit und Muße. Generell hab ich hier die Entwicklung nicht weiter vorangetrieben, obwohl ich die Webseite fast täglich aufrufe und benutze.</p> <p>Das Projekt nennt sich <a href="proxy.php?url=https://github.com/CydNoxzed/webferea">Webferea</a> und ist auch auf Github zu finden.</p> <p>Ein paar weitere Key-Features würden mir noch fehlen, aber alles läuft fast darauf hinaus einen kompletten Rewrite durchzuführen und am Ende hätte ich fast einen ganzen Feedreader für das Web geschrieben (was initial eigentlich nicht mein Ziel war). Eigentlich wäre es schön, wenn mir der Feedreader auch ein bisschen beim Kuratieren unterstützt. Wenn ich die Artikel eines Feeds gar nicht lese, könnte ich diesen auch mittelfristig entfernen. Die <a href="proxy.php?url=https://de.wikipedia.org/wiki/%C3%96konomie_der_Aufmerksamkeit">Aufmerksamkeitsökonomie</a> macht auch bei den Feeds nicht halt und man muss sich schon ein bisschen Zügeln, um nicht den ganzen Tag im Feedreader zu verbringen.</p> <p>Hierfür hätte ich auch einige Ideen im Hinterkopf. Vielleicht bringe ich diese auch mal hier im Blog unter.</p><ol class="simple-footnotes"><li id="sf-liferea-mobil-nutzen-1">Damit meine ich auch Atom- oder andere Web-Feeds. Das dass von technischer Seite nicht ganz passt, ist mir bewusst. <a href="proxy.php?url=#sf-liferea-mobil-nutzen-1-back" class="simple-footnote-back">↩︎</a></li><li id="sf-liferea-mobil-nutzen-2">Heutzutage übertrage ich schonmal 80 - 160 GiB pro Monat, auf meinem Desktoprechner allein. Der Großteil wird hierbei sicherlich aus Videos und Cloudanbindung bestehen. Sollte ich bei Gelegenheit vielleicht mal auswerten. <a href="proxy.php?url=#sf-liferea-mobil-nutzen-2-back" class="simple-footnote-back">↩︎</a></li><li id="sf-liferea-mobil-nutzen-3">Vermutlich Thunderbird, wenn mich meine Erinnerungen nicht täuschen. <a href="proxy.php?url=#sf-liferea-mobil-nutzen-3-back" class="simple-footnote-back">↩︎</a></li><li id="sf-liferea-mobil-nutzen-4">Von den 150 Feeds sind fast die Hälfte Feeds von Audio-Podcasts und YT-Videos. <a href="proxy.php?url=#sf-liferea-mobil-nutzen-4-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedThu, 18 Feb 2021 00:00:00 +0100tag:noxzed.de,2021-02-18:/2021/liferea-mobil-nutzen/projectspythonflaskrssprojectswebfereaPost Folderhttps://noxzed.de/2021/post-folder/<p>Der Zwang Speicherplatz zu sparen und Dateien ohne Umwege auf einen anderen Computer zu übertragen tragen zu ungewöhnlichen Lösungen bei.</p> <p>Um neue Dateien von meinem Computer auf mein Mediaserver zu übertragen, muss dieses gerade laufen, andernfall kann ich keine Dateien rüberkopieren.</p> <p>Im manuellen Modus heißt das: Wenn der Mediaserver läuft, mounte ich dessen SD-Karte und kopiere manuell die Videos/Bilder in den entsprechenden Ordner. Wenn dieser nicht läuft, kann ich auch nichts kopieren.</p> <p>Um diesen Misstand zu beseitigen, habe ich mich letzten Jahr auf die Suche nach einer Lösung hierfür gemacht.</p> <h2>Lösung finden</h2> <p>Die einfachste aller Varianten wäre vermutlich <a href="proxy.php?url=https://syncthing.net/">Syncthing</a>. Ein Folder auf beiden Geräten verbinden und automatisch synchronisieren lassen, wenn beide Geräte laufen. Der Haken an der Sache ist jedoch der, dass ich dann den gesamten Speicherverbrauch auf meinem Media-Server auch auf meinem Desktop-Rechner verursache.</p> <p>Aus Backup-Sicht sicherlich ganz gut, aus der Sicht des Speicherplatzverbrauchs jedoch nicht ganz so gut.</p> <p>Unidirektionaler Sync mit <a href="proxy.php?url=https://rsync.samba.org/">rsync</a>? Dagegen habe ich mich auch entschieden, ich kann mich jedoch leider nicht an die Gründe hierfür erinnern. Vermutlich fiel rsync raus, da es die Daten lokal nicht löscht oder abändert und ich dann die Referenz verliere, welche Daten bereits übertragen worden sind.</p> <p>Nachdem ich kein Tool finden konnte, welches meinen Anforderungen entsprach, habe ich mich hingesetz und schnell ein eigenes Skript geschrieben.<br> Das Ergebnis kann man sich weiter unten im Post anschauen.</p> <h2>Was macht es?</h2> <p>Man konfiguriert einen Ordner, dessen Dateien und Unterordner auf das andere System übertragen werden sollen. Aktuell ist das nur via SFTP möglich. Im zu übertragenen Ordner erstellt man eine Konfigurationsdatei <code>.postfolderrc</code> welche die Verbindungseinstellungen des Zielsystems enthalten, sowie den Zielordner für die Übertragung.</p> <p>Das Skript bekommt den Quellordner als Parameter übergeben und sucht sich dort die Konfigurationsdatei. Danach werden alle darin enthaltenen Dateien und Ordner rekursiv in den Zielordner übertragen. Wenn eine Datei übertragen wurde, dann wird diese im Quellordner gelöscht und mit einer leeren Datei selben Namens ersetzt. Zur Unterscheidung bekommt diese leere Datei noch das Suffix ».postfolder«, um diese auch auf den ersten Blick als bereits übertragen zu erkennen.</p> <p>Wenn das Skript als Cron gestartet wurde, wird jede Minute geprüft ob der Zielorder erreichbar ist und dann alle noch nicht übertragenen Dateien kopiert.</p> <p>Damit es durch den Cron nicht mehrfach ausgeführt wird (zb. weil gerade eine größere Datei übertragen wird), wird in <code>/tmp/</code> ein Pidfile gesetzt und somit die doppelte Ausführung des Skripts verhindert.</p> <p>Das Skript loggt die Übertragung aller Dateien mit und schreibt am Ende der Übertragung noch den verbleibenden freien Speicherplatz auf dem Zielgerät in das Logfile.</p> <h2>Installation</h2> <p>Folgende Pakete müssen für das Skript installiert werden:</p> <ul> <li>pysftp</li> <li>yaml</li> <li>pid</li> </ul> <p>oder ganz einfach via pip:</p> <div class="highlight"><pre><span></span><code>$<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>pysftp<span class="w"> </span>yaml<span class="w"> </span>pid </code></pre></div> <p>Das Skript selbst kann in einen beliebigen Ordner gelegt und<br> ausführbar (<code>$ chmod +x post_folder.py</code>) gemacht werden.</p> <h2>Konfiguration</h2> <p>Im Ordner der hochgeladen werden soll, muss die versteckte Datei <code>.postfolderrc</code> liegen. Folgender Inhalt muss enthalten sein:</p> <div class="highlight"><pre><span></span><code><span class="n">adapter</span><span class="o">:</span><span class="w"> </span><span class="n">sftp</span> <span class="n">adapter</span><span class="o">-</span><span class="n">config</span><span class="o">:</span> <span class="w"> </span><span class="n">username</span><span class="o">:</span><span class="w"> </span><span class="n">rpi</span> <span class="w"> </span><span class="n">password</span><span class="o">:</span><span class="w"> </span><span class="n">rpipassword</span> <span class="w"> </span><span class="n">host</span><span class="o">:</span><span class="w"> </span><span class="mf">192.168</span><span class="o">.</span><span class="mf">1.100</span> <span class="w"> </span><span class="n">port</span><span class="o">:</span><span class="w"> </span><span class="mi">22</span> <span class="n">remote_directory</span><span class="o">:</span><span class="w"> </span><span class="sr">/home/rpi/</span><span class="n">remotedirectory</span> </code></pre></div> <p>Falls die Dateien automatisch hochgeladen werden sollen (empfohlen), muss ein Eintrag in die Crontab erfolgen:</p> <div class="highlight"><pre><span></span><code>* * * * * /path/to/post_folder.py &quot;/path/to/directory/that/should/be/uploaded/&quot; &gt;&gt; ~/cron.log 2&gt;&amp;1 </code></pre></div> <h2>Code</h2> <div class="highlight"><pre><span></span><code><span class="c1"># -*- coding: utf-8 -*-</span> <span class="c1">#</span> <span class="c1"># post_folder.py - Transfer files/folders to another remote server</span> <span class="c1"># Copyright (C) 2021 Cyd Noxzed</span> <span class="c1"># </span> <span class="c1"># This program is free software: you can redistribute it and/or modify</span> <span class="c1"># it under the terms of the GNU General Public License as published by</span> <span class="c1"># the Free Software Foundation, either version 3 of the License, or</span> <span class="c1"># (at your option) any later version.</span> <span class="c1"># </span> <span class="c1"># This program is distributed in the hope that it will be useful,</span> <span class="c1"># but WITHOUT ANY WARRANTY; without even the implied warranty of</span> <span class="c1"># MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the</span> <span class="c1"># GNU General Public License for more details.</span> <span class="c1"># </span> <span class="c1"># You should have received a copy of the GNU General Public License</span> <span class="c1"># along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;.</span> <span class="kn">import</span> <span class="nn">os</span><span class="o">,</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">glob</span> <span class="kn">import</span> <span class="nn">logging</span> <span class="kn">import</span> <span class="nn">yaml</span> <span class="kn">import</span> <span class="nn">pysftp</span> <span class="kn">from</span> <span class="nn">pid</span> <span class="kn">import</span> <span class="n">PidFile</span><span class="p">,</span> <span class="n">PidFileAlreadyLockedError</span> <span class="k">class</span> <span class="nc">GenericSyncer</span><span class="p">:</span> <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">local_folder_path</span><span class="p">,</span> <span class="n">path_to_remote_folder</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">reference_suffix</span> <span class="o">=</span> <span class="s2">&quot;.postfolder&quot;</span> <span class="bp">self</span><span class="o">.</span><span class="n">temp_suffix</span> <span class="o">=</span> <span class="s2">&quot;.postfolder_tmp&quot;</span> <span class="bp">self</span><span class="o">.</span><span class="n">local_folder_path</span> <span class="o">=</span> <span class="n">local_folder_path</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_to_remote_folder</span> <span class="o">=</span> <span class="n">path_to_remote_folder</span> <span class="k">def</span> <span class="nf">get_files_to_sync</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">path</span><span class="p">):</span> <span class="n">files</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&quot;/&quot;</span><span class="p">):</span> <span class="n">path</span> <span class="o">=</span> <span class="s1">&#39;./&#39;</span> <span class="o">+</span> <span class="n">path</span> <span class="k">for</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">path</span> <span class="o">+</span> <span class="s1">&#39;**&#39;</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">True</span><span class="p">):</span> <span class="n">cleaned_filename</span> <span class="o">=</span> <span class="n">filename</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">cleaned_filename</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">cleaned_filename</span><span class="o">.</span><span class="n">endswith</span><span class="p">(</span> <span class="bp">self</span><span class="o">.</span><span class="n">reference_suffix</span><span class="p">):</span> <span class="n">files</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">cleaned_filename</span><span class="p">)</span> <span class="k">return</span> <span class="n">files</span> <span class="k">def</span> <span class="nf">replace_with_reference</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">file</span><span class="p">):</span> <span class="n">os</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">local_folder_path</span> <span class="o">+</span> <span class="n">file</span><span class="p">)</span> <span class="nb">open</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">local_folder_path</span> <span class="o">+</span> <span class="n">file</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">reference_suffix</span><span class="p">,</span> <span class="s1">&#39;a&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">close</span><span class="p">()</span> <span class="k">def</span> <span class="nf">sync</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">pass</span> <span class="k">class</span> <span class="nc">SftpSyncer</span><span class="p">(</span><span class="n">GenericSyncer</span><span class="p">):</span> <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">local_folder_path</span><span class="p">,</span> <span class="n">path_to_remote_folder</span><span class="p">,</span> <span class="n">adapter_configuration</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="n">local_folder_path</span><span class="p">,</span> <span class="n">path_to_remote_folder</span><span class="p">)</span> <span class="bp">self</span><span class="o">.</span><span class="n">config</span> <span class="o">=</span> <span class="n">adapter_configuration</span> <span class="bp">self</span><span class="o">.</span><span class="n">connection</span> <span class="o">=</span> <span class="kc">None</span> <span class="n">logfile</span> <span class="o">=</span> <span class="n">local_folder_path</span> <span class="o">+</span> <span class="s1">&#39;logfile&#39;</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">reference_suffix</span> <span class="n">logging</span><span class="o">.</span><span class="n">basicConfig</span><span class="p">(</span> <span class="n">filename</span><span class="o">=</span><span class="n">logfile</span><span class="p">,</span> <span class="n">level</span><span class="o">=</span><span class="n">logging</span><span class="o">.</span><span class="n">INFO</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s1">&#39;</span><span class="si">%(asctime)s</span><span class="s1"> | </span><span class="si">%(name)s</span><span class="s1"> | </span><span class="si">%(levelname)s</span><span class="s1"> | </span><span class="si">%(message)s</span><span class="s1">&#39;</span><span class="p">,</span> <span class="n">datefmt</span><span class="o">=</span><span class="s1">&#39;%Y-%m-</span><span class="si">%d</span><span class="s1"> %H:%M:%S&#39;</span> <span class="p">)</span> <span class="k">def</span> <span class="nf">sync</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">files</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_files_to_sync</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">local_folder_path</span><span class="p">)</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">files</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span> <span class="k">with</span> <span class="n">pysftp</span><span class="o">.</span><span class="n">Connection</span><span class="p">(</span><span class="o">**</span><span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="p">)</span> <span class="k">as</span> <span class="n">sftp</span><span class="p">:</span> <span class="k">for</span> <span class="n">file</span> <span class="ow">in</span> <span class="n">files</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">upload_file</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">sftp</span><span class="p">)</span> <span class="n">logging</span><span class="o">.</span><span class="n">info</span><span class="p">(</span> <span class="s2">&quot;finish upload - available space on remove device: </span><span class="si">{}</span><span class="s2">&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">get_available_space</span><span class="p">(</span><span class="n">sftp</span><span class="p">,</span> <span class="s1">&#39;/&#39;</span><span class="p">)))</span> <span class="k">def</span> <span class="nf">get_available_space</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">sftp</span><span class="p">,</span> <span class="n">device</span><span class="p">):</span> <span class="n">output</span> <span class="o">=</span> <span class="n">sftp</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&quot;df -H --output=avail /&quot;</span><span class="p">)</span> <span class="n">available</span> <span class="o">=</span> <span class="n">output</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s1">&#39;utf8&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">return</span> <span class="n">available</span> <span class="k">def</span> <span class="nf">upload_file</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">file</span><span class="p">,</span> <span class="n">sftp</span><span class="p">):</span> <span class="n">logging</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;upload [</span><span class="si">{}</span><span class="s2">] -&gt; [</span><span class="si">{}</span><span class="s2">/</span><span class="si">{}</span><span class="s2">]&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_to_remote_folder</span><span class="p">,</span> <span class="n">file</span><span class="p">))</span> <span class="n">sftp</span><span class="o">.</span><span class="n">chdir</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_to_remote_folder</span><span class="p">)</span> <span class="c1"># chdir to remote folder</span> <span class="bp">self</span><span class="o">.</span><span class="n">create_parent_dirs</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">sftp</span><span class="p">)</span> <span class="n">sftp</span><span class="o">.</span><span class="n">chdir</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_to_remote_folder</span><span class="p">)</span> <span class="c1"># chdir to remote folder</span> <span class="n">success</span> <span class="o">=</span> <span class="n">sftp</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">local_folder_path</span> <span class="o">+</span> <span class="n">file</span><span class="p">,</span> <span class="n">file</span><span class="p">)</span> <span class="n">remote_size</span> <span class="o">=</span> <span class="n">success</span><span class="o">.</span><span class="n">st_size</span> <span class="n">local_size</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">getsize</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">local_folder_path</span> <span class="o">+</span> <span class="n">file</span><span class="p">)</span> <span class="k">if</span> <span class="n">local_size</span> <span class="o">==</span> <span class="n">remote_size</span><span class="p">:</span> <span class="c1"># successfully transfered</span> <span class="bp">self</span><span class="o">.</span><span class="n">replace_with_reference</span><span class="p">(</span><span class="n">file</span><span class="p">)</span> <span class="n">logging</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;upload finished: [</span><span class="si">{}</span><span class="s2">]&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">file</span><span class="p">))</span> <span class="k">return</span> <span class="kc">True</span> <span class="k">def</span> <span class="nf">create_parent_dirs</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">file</span><span class="p">,</span> <span class="n">sftp</span><span class="p">):</span> <span class="n">parts</span> <span class="o">=</span> <span class="n">file</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">parts</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">]:</span> <span class="c1"># dont use the last part as directory</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">sftp</span><span class="o">.</span><span class="n">isdir</span><span class="p">(</span><span class="n">p</span><span class="p">):</span> <span class="n">sftp</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="n">sftp</span><span class="o">.</span><span class="n">chdir</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="k">return</span> <span class="kc">True</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> <span class="n">defaultrc</span> <span class="o">=</span> <span class="s2">&quot;.postfolderrc&quot;</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span> <span class="n">path_to_configuration</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">if</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isdir</span><span class="p">(</span><span class="n">path_to_configuration</span><span class="p">):</span> <span class="n">path_to_configuration</span> <span class="o">=</span> <span class="n">path_to_configuration</span><span class="o">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)</span> <span class="o">+</span> <span class="s1">&#39;/&#39;</span> <span class="o">+</span> <span class="n">defaultrc</span> <span class="k">else</span><span class="p">:</span> <span class="n">path_to_configuration</span> <span class="o">=</span> <span class="n">defaultrc</span> <span class="n">path_to_local_folder</span> <span class="o">=</span> <span class="n">path_to_configuration</span><span class="p">[:(</span><span class="mi">0</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">defaultrc</span><span class="p">))]</span> <span class="n">yamlfile</span> <span class="o">=</span> <span class="n">yaml</span><span class="o">.</span><span class="n">safe_load</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="n">path_to_configuration</span><span class="p">,</span> <span class="s1">&#39;r&#39;</span><span class="p">))</span> <span class="n">adapter_name</span> <span class="o">=</span> <span class="n">yamlfile</span><span class="p">[</span><span class="s1">&#39;adapter&#39;</span><span class="p">]</span> <span class="n">path_to_remote_folder</span> <span class="o">=</span> <span class="n">yamlfile</span><span class="p">[</span><span class="s1">&#39;remote_directory&#39;</span><span class="p">]</span> <span class="k">try</span><span class="p">:</span> <span class="k">with</span> <span class="n">PidFile</span><span class="p">(</span><span class="n">piddir</span><span class="o">=</span><span class="s1">&#39;/tmp/post_folder.pid&#39;</span><span class="p">):</span> <span class="n">adapter</span> <span class="o">=</span> <span class="kc">None</span> <span class="k">if</span> <span class="n">adapter_name</span> <span class="o">==</span> <span class="s1">&#39;sftp&#39;</span><span class="p">:</span> <span class="n">adapter</span> <span class="o">=</span> <span class="n">SftpSyncer</span><span class="p">(</span><span class="n">path_to_local_folder</span><span class="p">,</span> <span class="n">path_to_remote_folder</span><span class="p">,</span> <span class="n">yamlfile</span><span class="p">[</span><span class="s1">&#39;adapter-config&#39;</span><span class="p">])</span> <span class="k">if</span> <span class="n">adapter</span><span class="p">:</span> <span class="n">adapter</span><span class="o">.</span><span class="n">sync</span><span class="p">()</span> <span class="k">except</span> <span class="n">PidFileAlreadyLockedError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="k">pass</span> <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="n">template</span> <span class="o">=</span> <span class="s2">&quot;An exception of type </span><span class="si">{0}</span><span class="s2"> occurred. Arguments: </span><span class="si">{1!r}</span><span class="s2">&quot;</span> <span class="n">message</span> <span class="o">=</span> <span class="n">template</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="n">e</span><span class="p">)</span><span class="o">.</span><span class="vm">__name__</span><span class="p">,</span> <span class="n">e</span><span class="o">.</span><span class="n">args</span><span class="p">)</span> <span class="nb">print</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> <span class="k">pass</span> <span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span> <span class="n">main</span><span class="p">()</span> </code></pre></div> <h2>Fazit</h2> <p>Nächster Step wäre vermutlich auch die Löschung der Daten auf dem Media-Server, wenn ich die Referenzdateien (*.postfolder) auf dem Desktop-PC entferne.</p> <p>Da bin ich jedoch noch nicht dazu gekommen, bzw war es mir bisher noch nicht wichtig genug.</p> <p>Für Anregungen oder Verbesserungsvorschläge an dem Skript oder dem System an sich wäre ich übrigens echt dankbar.</p>Cyd NoxzedSun, 14 Feb 2021 00:00:00 +0100tag:noxzed.de,2021-02-14:/2021/post-folder/projectspythonprojectsEntity-Component-Systemhttps://noxzed.de/2021/entity-component-system/<p>Bei der Spieleentwicklung ist man dazu geneigt alles im Spiel objektorientiert umsetzen zu wollen. Es bietet sich ja eigentlich auch an. Eine Spielfigur, ein Healthpack oder ein Feuerball sind ja alles Objekte. Es gibt ein Hauptobjekt, von dem alle anderen Objekte abgeleitet und weiter spezialisiert werden.</p> <h2>Objekt-orientierter Ansatz</h2> <p>Auch ich hab das offensichtliche als gegeben hingenommen und so auch meine Entities im Spiel entsprechend aufgebaut:</p> <ul> <li>Actor (Hauptobjekt)<ul> <li>Pickups (aufsammelbare Gegenstände)</li> <li>Healthpack</li> <li>Ammo</li> <li>Pawn (Spielfigur)<ul> <li>Bot<ul> <li>Mob (Monster oder Beast)</li> </ul> </li> <li>Player (steuerbare Spielfigur)</li> </ul> </li> <li>Projektil<ul> <li>Feuerball</li> <li>Pfeil</li> </ul> </li> <li>Navigation (funktionale Spielpunkte)</li> <li>Teleporter</li> <li>Spawn (Einstiegspunkt für Pawns)</li> </ul> </li> </ul> <p>Abhängig von der Größe und dem Umfang des Spiels wird sich mit so einem Design früher oder später eine Klasse herauskristallisieren, welche zu einem großen Blob wird. Bei einem späteren Design wollte ich sowas umgehen und bestimmte Charakteristika in eigene Subklassen (Traits) auslagern. Es gab dann so Traits wie <code>Movable</code> (für Velocity und Bewegung), <code>Collidable</code> (für die Kollisionsberechnung), <code>Animation</code> (für die grafische Ausgabe), oder <code>Mortal</code> (für Health und so).</p> <p>Diese Trait-Klassen konnte ich dann – dank Mehrfachvererbung – einfach in die jeweiligen Klassen mit reinladen, der Ansatz ähnelt dabei <a href="proxy.php?url=https://de.wikipedia.org/w/index.php?title=Komposition_an_Stelle_von_Vererbung">Komposition</a>:</p> <div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="n">Pawn</span>(<span class="n">Actor</span>, <span class="n">Visible</span>, <span class="n">Collidable</span>, <span class="n">Movable</span>, <span class="n">Equipable</span>, <span class="n">Mortal</span>): <span class="s">&quot;&quot;&quot; A Pawn is every character or mob in the world &quot;&quot;&quot;</span> <span class="n">def</span> <span class="n">__init__</span>(<span class="nb">self</span>) -&gt; <span class="n">None:</span> <span class="n">Actor</span>.<span class="n">__init__</span>(<span class="nb">self</span>) <span class="n">Visible</span>.<span class="n">__init__</span>(<span class="nb">self</span>) <span class="n">Collidable</span>.<span class="n">__init__</span>(<span class="nb">self</span>) <span class="n">Movable</span>.<span class="n">__init__</span>(<span class="nb">self</span>) <span class="n">Equipable</span>.<span class="n">__init__</span>(<span class="nb">self</span>) <span class="n">Mortal</span>.<span class="n">__init__</span>(<span class="nb">self</span>) </code></pre></div> <p>An sich konnte ich mit diesem Design gut arbeiten. Teilweise hatten die Traits noch aktiv Properties, um zB. diese Unsichtbar zu machen oder die Kollisionserkennung zu deaktivieren.</p> <p>Probleme bekame ich erst dann, als ich die Objekte nach JSON serialisieren und im Anschluss wieder deserialisieren wollte. Notwendig wäre das für eine mögliche Implementierung eines Netzwerk-Multiplayers oder zum Speichern des Spielstandes. Für beides benötigte ich nicht zwangsläufig JSON und könnte es auch über <a href="proxy.php?url=https://docs.python.org/3/library/pickle.html">Pickle</a> durchführen, wobei das jetzt nicht der sicherste Weg wäre.</p> <p>Serialisieren war nicht das Problem: Alle Properties auslesen (<code>pawn.__dict__</code>) und die Sache ist schon erledigt.<br> Funktioniert zumindest in 90 % der Fälle (wenn es sich um normale Datentypen handelt).</p> <p>Die Deserialisierung gestaltete sich jedoch schwerer. Eine leere <code>Pawn</code> Klasse erstellen und diese dann Stück für Stück mit den jeweiligen Properties füllen? Position (zwei floats), Name der Figur (string) und Lebenspunkte (int) gehen ja noch. Aber wenn ein Trait Binärdaten (Bild) enthält (<code>Visible</code>), muss ich einen Sonderweg gehen und die abgespeicherte Referenz auf die Datei zum Laden der Animation nutzen. Das funktioniert für vielleicht ein paar Sonderwege, aber früher oder später ist der zusätzliche Aufwand zu groß, da diese bei fast jedem Feature berücksichtigt werden müssen.</p> <p>Nach kurzen Recherchen bin ich bereits auf eine Lösung dieses Problems gekommen: <a href="proxy.php?url=https://en.wikipedia.org/w/index.php?title=Entity_component_system">Entity-Component-System</a>.</p> <h2>Entity-Component-System</h2> <p>Es gibt im Netz bereits viele gute Beschreibungen der unterschiedlichsten Flavours von ECS (zB die von <a href="proxy.php?url=http://www.roguebasin.com/index.php?title=Entity_Component_System">roguebasin.com</a>), deswegen nur kurz die Grundlagen erklärt:</p> <ul> <li>Entity: ID zur Kennzeichnung einer Entity in der Spielwelt (zb ein eindeutiger String oder eine aufsteigende Zahl)</li> <li>Component: Ein einfacher Datencontainer, welcher die Daten für die jeweilige Domäne enthält (zB. eine einfache Klasse oder ein C-Struct)</li> <li>System: Klassen/Handler, welche die Componenten abhängig zum Spielverlauf erstellen und modifizieren</li> </ul> <p>Jede Entity kann somit eine Sammlung oder Zusammenstellung aus unterschiedlichen Components sein. Abhängig davon, ob diese Komponente für die jeweilige Entity benötigt wird. Das Entity Pickup ist Aufsammelbar (Collectible), aber unbeweglich (Movement).</p> <table> <thead> <tr> <th style="text-align: right;"></th> <th style="text-align: center;">Position</th> <th style="text-align: center;">Movement</th> <th style="text-align: center;">Controls</th> <th style="text-align: center;">Collision</th> <th style="text-align: center;">Collectible</th> </tr> </thead> <tbody> <tr> <td style="text-align: right;">Player</td> <td style="text-align: center;">X</td> <td style="text-align: center;">X</td> <td style="text-align: center;">X</td> <td style="text-align: center;">X</td> <td style="text-align: center;"></td> </tr> <tr> <td style="text-align: right;">Enemy</td> <td style="text-align: center;">X</td> <td style="text-align: center;">X</td> <td style="text-align: center;"></td> <td style="text-align: center;">X</td> <td style="text-align: center;"></td> </tr> <tr> <td style="text-align: right;">Pickup</td> <td style="text-align: center;">X</td> <td style="text-align: center;"></td> <td style="text-align: center;"></td> <td style="text-align: center;">X</td> <td style="text-align: center;">X</td> </tr> <tr> <td style="text-align: right;">Projectile</td> <td style="text-align: center;">X</td> <td style="text-align: center;">X</td> <td style="text-align: center;"></td> <td style="text-align: center;">X</td> <td style="text-align: center;"></td> </tr> </tbody> </table> <p>Jede Component hat ein oder mehrere Systeme, welche die Daten darin modifizieren können. Diese Systeme iterieren einfach über alle dafür zuständigen Componenten und verarbeiten diese. Durch das dynamische Hinzufügen oder Löschen von Componenten an einer Entity ist man extrem flexibel. Soll eine Entity nicht mehr dargestellt werden? Einfach die Grafik-Komponente entfernen. Soll die Steuerung vom Spieler auf den Gegener übergehen (Mind-Control o.ä.)? Dann muss die zuständige Komponente (im oberen Beispiel <code>Controls</code>) am Player gelöscht und am Enemy hinterlegen werden.</p> <p>Das <a href="proxy.php?url=http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/">Upgrade auf ECS</a> hat mich durch die bereits vorhandene Domänen-Trennung der Traits nicht groß an Zeit gekostet. Es ging mehr Zeit damit verloren eine geeignete Python-ECS-Library zu finden oder gar selbst eine zu schreiben. Ich habe mich letzten Endes für <a href="proxy.php?url=https://github.com/benmoran56/esper">esper</a>, da es genau meine Anforderungen erfüllte:</p> <ul> <li>Component ist normales Objekt</li> <li>die System Hauptklasse hat ein simples Interface</li> <li>als Entities kann ich UUIDs verwenden (hierzu musste ich jedoch esper ein bisschen umbauen, da hier per Default ein Integer hochgezählt wird)</li> </ul> <p>Der simple und elegante Ansatz macht ECS zu einem nicht zu unterschätzenden Pattern in der Spieleprogrammierung. Die domänenbasierte Trennung von Daten und Logik, hat es mir einfach gemacht, die ganze Spielewelt (alle Entities und die zugehörigen Components) zu serialisieren und daraus wieder eine komplette Spielewelt aufzubauen. Laden und Speichern konnte ich somit binnen kürzester Zeit implementieren.</p> <p>Wobei ich auch dazu sagen muss, dass ich auf Binärdaten innerhalb der Components verzichtet habe. Die Render-Compontent enthält in meinem Ansatz lediglich den Namen der Animation, den aktuellen Frame, sowie die Restdauer des Frames. Mit der Serialisierung war auch die Erstellung eines rudimentären Netzwerkcodes keine gewaltige Aufgabe mehr.</p> <p>Als weiterführendes Thema kann ich auch den <a href="proxy.php?url=https://www.youtube.com/watch?v=JxI3Eu5DPwE">Talk »Is There More to Game Architecture than ECS?« von Bob Nystrom</a> auf der <a href="proxy.php?url=https://roguelike.club/">Roguelike Celebration 2018</a></p>Cyd NoxzedWed, 10 Feb 2021 00:00:00 +0100tag:noxzed.de,2021-02-10:/2021/entity-component-system/gamedevgamedevweirdDirektor und Szenenhttps://noxzed.de/2021/direktor-und-szenen/<p>Wie ein Theaterstück kann auch ein Spiel in mehrere Szenen unterteilt werden. Hierbei geht es jedoch weniger um einzelne Levelabschnitte, sondern um den allgemeinen Workflow.</p> <p>Zum Beispiel:</p> <div class="highlight"><pre><span></span><code> +-------+ +------+ +---------+ +-----------------+ +---------+ +------------+ | Intro +--&gt;+ Menu +--&gt;+ Level 1 +--&gt;+ Zwischensequenz +--&gt;+ Level 2 +--&gt;+ Endsequenz | +-------+ +------+ +---------+ +-----------------+ +---------+ +------------+ ^ | | | | | +--------------+ | | | +------&gt;+ Todessequenz +&lt;------------+ | | +--------------+ | | | | +-----------+ | +---------------&gt;+ Highscore +&lt;---------------------------------+ +-----------+ </code></pre></div> <p>Wer sich hierbei an eine Statemaschine erinnert fühlt, hat das Grundprinzip vermutlich verstanden.</p> <p>Jede Szene verarbeitet hierbei für sich den getätigten Input verarbeitet diesen zu einer Ausgabe. Als Objekt (oder Interface) sähe eine einfache Szene etwa so aus:</p> <div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">Scene</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span> <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">pass</span> <span class="k">def</span> <span class="nf">process_input</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_events</span> <span class="o">=</span> <span class="kc">None</span><span class="p">):</span> <span class="w"> </span><span class="sd">&quot;&quot;&quot; Process the input of the current loop &quot;&quot;&quot;</span> <span class="k">pass</span> <span class="k">def</span> <span class="nf">update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dt</span><span class="p">):</span> <span class="w"> </span><span class="sd">&quot;&quot;&quot; Update the current scene based on the given delta time in milliseconds &quot;&quot;&quot;</span> <span class="k">pass</span> <span class="k">def</span> <span class="nf">render</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">screen</span><span class="p">:</span> <span class="n">pygame</span><span class="o">.</span><span class="n">Surface</span><span class="p">):</span> <span class="w"> </span><span class="sd">&quot;&quot;&quot; Render the scene to the screen &quot;&quot;&quot;</span> <span class="k">pass</span> </code></pre></div> <p>Um die ganzen verschiedenen Szenen zu verwalten wird ein Direktor benötigt. Jedes Spiel sollte nur einen davon haben, dieser enthält den normalen Gameloop und initialisiert auch das Fenster.</p> <p>Ein einfacher Direktor könnte etwa so aussehen:</p> <div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">Director</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span> <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">display_size</span><span class="p">:</span> <span class="nb">list</span><span class="p">,</span> <span class="n">fps</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">30</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">display_size</span> <span class="o">=</span> <span class="n">display_size</span> <span class="bp">self</span><span class="o">.</span><span class="n">fps</span> <span class="o">=</span> <span class="n">fps</span> <span class="bp">self</span><span class="o">.</span><span class="n">window</span> <span class="o">=</span> <span class="kc">None</span> <span class="bp">self</span><span class="o">.</span><span class="n">scenes</span> <span class="o">=</span> <span class="p">{}</span> <span class="bp">self</span><span class="o">.</span><span class="n">running_scene</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span> <span class="bp">self</span><span class="o">.</span><span class="n">quit_attempt</span> <span class="o">=</span> <span class="kc">False</span> <span class="n">pygame</span><span class="o">.</span><span class="n">init</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">display</span> <span class="o">=</span> <span class="n">pygame</span><span class="o">.</span><span class="n">display</span><span class="o">.</span><span class="n">set_mode</span><span class="p">(</span><span class="n">display_size</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">clock</span> <span class="o">=</span> <span class="n">pygame</span><span class="o">.</span><span class="n">time</span><span class="o">.</span><span class="n">Clock</span><span class="p">()</span> <span class="k">while</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">quit_attempt</span><span class="p">:</span> <span class="c1"># Main loop</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">running_scene</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">scenes</span><span class="p">:</span> <span class="k">continue</span> <span class="n">scene</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">scenes</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">running_scene</span><span class="p">]</span> <span class="n">dt</span> <span class="o">=</span> <span class="n">clock</span><span class="o">.</span><span class="n">tick</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">fps</span><span class="p">)</span> <span class="c1"># process input</span> <span class="n">input_events</span> <span class="o">=</span> <span class="n">pygame</span><span class="o">.</span><span class="n">event</span><span class="o">.</span><span class="n">get</span><span class="p">()</span> <span class="n">scene</span><span class="o">.</span><span class="n">process_input</span><span class="p">(</span><span class="n">input_events</span><span class="p">)</span> <span class="c1"># update</span> <span class="n">scene</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">dt</span><span class="p">)</span> <span class="c1"># render</span> <span class="bp">self</span><span class="o">.</span><span class="n">display</span><span class="o">.</span><span class="n">fill</span><span class="p">((</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="n">scene</span><span class="o">.</span><span class="n">render</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">display</span><span class="p">)</span> <span class="n">pygame</span><span class="o">.</span><span class="n">display</span><span class="o">.</span><span class="n">flip</span><span class="p">()</span> </code></pre></div> <p>Als Basis würden diese beiden Klassen erstmal ausreichen. Initialisieren und starten könnte man das ganze dann so:</p> <div class="highlight"><pre><span></span><code><span class="n">scene1</span> <span class="o">=</span> <span class="n">Scene</span><span class="p">()</span> <span class="n">scene2</span> <span class="o">=</span> <span class="n">Scene</span><span class="p">()</span> <span class="n">director</span> <span class="o">=</span> <span class="n">Director</span><span class="p">((</span><span class="mi">800</span><span class="p">,</span> <span class="mi">600</span><span class="p">),</span> <span class="mi">30</span><span class="p">)</span> <span class="n">director</span><span class="o">.</span><span class="n">scenes</span> <span class="o">=</span> <span class="p">{</span><span class="s1">&#39;1&#39;</span><span class="p">:</span> <span class="n">scene1</span><span class="p">,</span> <span class="s1">&#39;2&#39;</span><span class="p">:</span> <span class="n">scene2</span><span class="p">}</span> <span class="n">director</span><span class="o">.</span><span class="n">running_scene</span> <span class="o">=</span> <span class="s1">&#39;1&#39;</span> <span class="n">director</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> </code></pre></div> <p>Gewechselt kann die Szene dann einfach durch das abhändern der Variable <code>director.running_scene = '2'</code>.</p> <p>Für das Speichern der Szenen reicht ein <code>dict</code> erstmal aus, schöner wäre es jedoch trotzdem wenn man hierfür eine geeignete <a href="proxy.php?url=https://docs.python.org/3/library/collections.html">collection</a> verwendet und diese über eine separate Funktion <code>director.add(scene1)</code> füllt.<br> Der Name der Szene könnte auch in der Szene selbst gespeichert werden. Den Direktor könnte man auch an die Scene selbst mit übergeben und dann kann die Scene bei Bedarf die nächste Szene aufrufen.</p> <p>Den Übergang von einer Szene in die Nächste kann man mit Transitions auch spannender gestalten. <a href="proxy.php?url=https://python.cocos2d.org/doc/programming_guide/transitions.html">cocos2d's scenes Modul</a> bietet hier einige coole und interessante <a href="proxy.php?url=https://python.cocos2d.org/doc/api/cocos.scenes.transitions.html#module-cocos.scenes.transitions">Übergänge</a> an. Prinzipiell basieren diese immer darauf ein Bild in ein anderes zu überführen.</p> <p>Möglichkeiten zum Optimieren und Anpassen bieten die beiden Klassen definitiv.</p>Cyd NoxzedFri, 05 Feb 2021 00:00:00 +0100tag:noxzed.de,2021-02-05:/2021/direktor-und-szenen/gamedevgamedevWas für ein Spiel soll es sein?https://noxzed.de/2021/was-fur-ein-spiel-soll-es-sein/<p>Sich ein genaues Ziel für ein Spiel zu definieren ist üblicherweise der härteste Teil. Man sollte sich nicht zu viel vornehmen und alle möglichen kleinen Schritte vorher schon grob durchplanen.</p> <p>Ich möchte ein Top-down Multiplayer Dungeon Crawler kreieren, bei dem man über LAN (oder ggf. Internet) gemeinsam mehrere Ebenen einer computergenerierten Dungeon überwinden muss. Zusätzlich soll das Spiel später um einen Deathmatch- oder CTF-Modus erweitert werden können.</p> <p>In den letzten Jahren habe ich bereits mehrere Anläufe unternommen, so ein Spiel zu schreiben. Der erste Anlauf war straight-forward mit <a href="proxy.php?url=https://www.pygame.org/">pygame</a>. Aufgrund schlechtem Codedesigns wurde es immer schwieriger schnell und einfach neue Funktionalitäten in das bereits bestehende Spiel einzubauen. Mein Ziel konnte ich damit nicht erreichen.</p> <p>Bei meinem zweiten Anlauf setzte ich auf <a href="proxy.php?url=http://python.cocos2d.org/">cocos2d</a>. Hauptgrund hierfür war die einfachere handhabe mit transparenten Grafiken und das einfache aber umfangreiche <a href="proxy.php?url=http://python.cocos2d.org/doc/api/cocos.actions.html">Actions-Submodule</a>. Leider wurde die Bibliothek zu dieser Zeit nicht weiterentwickelt und es gab für einige Anwendungsfälle keine notwendigen Funktionalitäten. Beispielweise eine stärkere Unterstützung zum Lesen von Layern und Properties des <a href="proxy.php?url=https://www.mapeditor.org/">tiled</a>-Editors. Abgesehen davon ist python-cocos2d eine gute Bibliothek zum Schreiben von Spielen.</p> <p>Der dritte Anlauf war dann der, alle notwendigen und umfangreicheren Funktionen aus bereits existierenden Libraries zu ziehen und diese in ein Framework einzubauen, welches ich dann dazu benutze das Spiel selbst umzusetzen. Hierzu habe ich wieder auf pygame zugegriffen, da es hierfür eine größere und aktiviere Community gibt, auf deren gepflegten Code man aufbauen kann. Mit dem Framework wollte ich dann auch einen großen Wurf machen (umfangreiche Dokumentation und alles auch eine 100%ige Testcoverage).</p> <p>Mit diesem Vorhaben habe ich mich übernommen, auch wenn ich hierbei viel gelernt habe, bei der Implementation der einzelnen Subsysteme. Ziel sollte es defintiv nicht sein, eine »Engine« zu schreiben, sondern das Spiel muss das Ziel sein<sup id="sf-was-fur-ein-spiel-soll-es-sein-1-back"><a href="proxy.php?url=#sf-was-fur-ein-spiel-soll-es-sein-1" class="simple-footnote" title="Write Games, Not Engines">1</a></sup>. Das habe ich dann nach einiger Zeit auch gemerkt.</p> <p>Der Fokus liegt jetzt jedenfalls auf ein Spiel, auch wenn ich mich auch stark mit dem dahinterliegenden Design auseinandersetze. Immerhin will ich ja später auch ganz leicht und schnell neue Elemente einfügen können. Den genauen Umfang hab ich bisher aber leider auch noch nicht definiert.</p><ol class="simple-footnotes"><li id="sf-was-fur-ein-spiel-soll-es-sein-1"><a href="proxy.php?url=https://geometrian.com/programming/tutorials/write-games-not-engines/">Write Games, Not Engines</a> <a href="proxy.php?url=#sf-was-fur-ein-spiel-soll-es-sein-1-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedSun, 31 Jan 2021 00:00:00 +0100tag:noxzed.de,2021-01-31:/2021/was-fur-ein-spiel-soll-es-sein/gamedevgamedevweirdHello Worldhttps://noxzed.de/2021/hello-world/<p>Nach längerem Überlegen habe ich mir nun doch einen Blog aufgesetzt und diesen auch Online gestellt<sup id="sf-hello-world-1-back"><a href="proxy.php?url=#sf-hello-world-1" class="simple-footnote" title="Wenn der Leser dies im WWW liest, dann deutet dass jedenfalls stark darauf hin.">1</a></sup>.</p> <p>Der Blog dient dazu meine Gedanken zu verschiedenen Themen zu ordnen und mir auch beim Reflektieren zu helfen. Außerdem kann es ganz interessant sein, Jahre später diese Posts mal wieder zu lesen. Vielleicht lösen meine Texte auch den einen oder anderen Gedankengang beim Leser aus.</p> <p>Ich habe mich dazu entschieden den Blog auf Deutsch zu schreiben, da ich für mich selbst die Hemmschwelle möglichst niedrig halten wollte. Wobei ich mir beim Formulieren von Sätzen im deutschen nicht unbedingt leichter tue, zu oft beginne ich damit einen englischen Satz zu formulieren.</p> <p>Der Blog selbst besteht technisch aus statischen HTML-Seiten, welche direct vom Webserver ausgeliefert werden. Kein PHP, MySQL, CGI, etc ist hierfür notwendig. Um die Seite bei jeder Änderung neu zu bauen, wird im Hintergrund <a href="proxy.php?url=https://blog.getpelican.com/">Pelican</a>, ein »Static Site Generator«, verwendet.</p><ol class="simple-footnotes"><li id="sf-hello-world-1">Wenn der Leser dies im WWW liest, dann deutet dass jedenfalls stark darauf hin. <a href="proxy.php?url=#sf-hello-world-1-back" class="simple-footnote-back">↩︎</a></li></ol>Cyd NoxzedSat, 23 Jan 2021 00:00:00 +0100tag:noxzed.de,2021-01-23:/2021/hello-world/miscblogprojectsmisc