democracy [dɪˈmɒk.ɹə.si] (noun):
power of the people
On November 21st, 2024, (
I gave a talk about the software crisis), detailing its general impact on how we learn and practice the craft of software. It was equal parts philosophical and technical, with a focus on human-scale problems and how they affect networks of craftspeople and learners.
The central thesis of these talks was a simple message: something has to change, here's one direction we can go. The aim wasn't to shame others into submission, but rather to inspire people to start closely examining their craft under a new technical and philosophical lens.
When presenting a new method of anything, be it computation, communication, thought, or something that covers all three, alienating people is a great way to kill adoption. We have to connect with people, and meet them where they are. We're all speaking the same language, just different dialects. We must travel and traverse uneven terrain: there are amazing cultures there.
To that end, in this talk, I proposed a common language for communication and computation: rewriting. Out of all of the paradigms I surveyed, all of the different models of and methods of describing thinking about computation, this happened to be the one I found most universally intuitive. This talk was born of that research, utilizing my background in education and my connections with friends and family.
If we want to stop feeling like we're aliens to each-other, we must first find a way to communicate. I hope you enjoy the talk, and if it resonates with you, reach out. I'd love to hear from you.
-------
Good morning!
My name is Wryl, and today I'm going to talk to you about something near and dear to my heart.
The title of this talk is Democratizing Software, and this talk is the culmination of research, observations and principles I've cultivated through my journey with programming.
It'll have ups, it'll have downs, it'll have some new concepts, it'll have some old concepts, but my primary goal is this: show how we can democratize software. I want to enable people of all backgrounds to participate in the beauty of computation.
But first, we have to talk about some difficult things. We have to talk about how difficult it is to participate in the craft of software, and how it's akin to scaling mountains of concepts and abstractions.
We, as mountaineers, take for granted how easily we can traverse the terrain. We tolerate so much, because it’s the only way we can get from point A to point B. And personally, I’m very, very tired of hiking around.
Imagine how newcomers feel?
This is a common parable. Newcomers and curious folk come to us and ask: 'Where should I start?', and we'll respond with something like Python, or JavaScript, or defer and say 'Whatever you can get your hands on.'
Instead of pointing at some well-trodden paths, we ask them to survey the mountain range. To blaze their own trails, navigate the terrain on your own.
A lot of people get lost. And sometimes, they don’t come back.
And the reason they get lost is because of the effort required to scale these mountains.
The terrain here is hazardous, and often makes no navigational sense. Stacking concepts and abstractions on top of one another means that each layer is a new concept to be understood and internalized.
We also don't have the limitations that real mountains do: we can build arbitrarily nested structures, things that contain themselves, layers and layers of indirection. I don’t think we know how many people we’ve lost to this.
We're setting ourselves up to look like aliens, as curious individuals can't really understand what we do at a glance, if at all. They need a guide to help them up and down the mountain.
This is also bleeding into how we're writing software,as eventually, we'll end up writing things that we can't comprehend fully, only partially.
Blind spots can and will develop. The trails will disappear, overrun with brush. When you run out of energy to climb, nature and gravity take over in the worst ways.
And this compounds in so many different, unpredictable ways. Our lives are filled with certain flavors of chaos. We may not be able to comprehend what other people, practicing our common craft, are doing.
Context switching, emergencies, and the general flow of life drain what little energy we might have for crafting software. And it’s very, very difficult to recover that energy, and gear up for another climb.
Look at how burnout develops, and how long it takes to learn how to walk again, let alone climb the mountains we’re required to climb.
We cannot keep pace, even as experienced mountaineers. I fear we're hitting the limit. I certainly can't keep up anymore, even though I’ve tried.
New mountains appear every day, and we even have tools that generate new mountains for you, on-demand! We're getting to the point that two people, studying the same craft, can't talk to each other, or relate what they're working on to their friends and family.
We've turned something that is deeply, deeply human into something bordering inhumane.
What do we do about it? How do we even begin to clean any of this up?
How do we tear the mountains, the scaffolding, and all the unneeded complexity down, while staying in touch with basic intuition?
How do we do this without sacrificing the last 60 years of advancements? How do we build tools that can withstand chaos?
To even start, we need to talk about how humans learn explore, and internalize new things. We also need to talk about how they maintain and update that knowledge.
We need to have a good, solid grasp on how knowledge is constructed, and how much concepts weigh in our minds.
We have to really understand how much conceptual weight we have piled up in our heads.
Learning is often seen as the process of layering new, isolated concepts on top of each-other in the hope that they'll interlock. Once everything 'clicks' into place, you'll gain a greater understanding and intuition about things, right? That’s the hope, anyway.
We follow the same pattern in constructing software, layering abstractions on top of eachother in the hope that they'll click together.
But it's all the same: layering is how mountains are built.
In my observations, learning is less about layered, isolated, interlocking concepts, and more about connecting with pre-existing intuition from all over a person's mind.
Hobbies, habits, activities, interests: they’re all great, strong candidates. If someone loves to cook, I can connect algorithms to recipes.
The more prominent and strong the intuition, the more it can be used as a basis for new concepts.
Learning is less layering, and more web weaving: it's all the same cognitive material, just woven in different patterns.
To that end, this weaving of intuition can happen in a number of different ways, each of them individualized and deeply personal.
Learners have to be able to explore how they can weave their own intuition. We can offer a nurturing environment, and give them a push, but they need to be able to explore the space of possibilities.
They need to be able to rewind and fast forward, to pick apart alternatives, to examine and internalize cause and effect. They need to be able to play.
A grand majority of tools we hand to beginners can’t meet these requirements.
And it's not because it's an inherent flaw of software! It's because of the force of historical inertia.
These are the models we've used, this is how we teach software development, this is how we teach computation, this is how we make a living from it, and nothing can change.
Ever. It's frowned upon.
Which I think is absolutely wrong! We need to break this cycle of inertia, else we'll continue to be swept up in it.
To break the cycle, we need to look into how we lost pace with our tools, and how the 'old masters' developed their intuition.
I'm of the opinion that something went very, very wrong in the past 40 years, and it's been largely unaddressed, despite it being pervasive and visible.
You'll hear whispers of it in retrocomputing and low-level programming circles. It's as if it's forbidden, arcane knowledge. It’s not.
It’s just been forgotten.
There's this idea of growth versus mastery. The idea that, given some set of tools or resources, it takes some time to fully master their usage, to effectively utilize those tools and resources.
We see this everywhere. It’s why the demoscene is impressive, and why developers on older console generations shipped more technically impressive games at the end of a console’s lifecycle. They mastered the platform.
It may be days, weeks, months or years, but mastery can be attained! But if the tools keep growing in complexity, in features, and the criteria for mastery keeps getting larger, it becomes impossible to keep up.
It's gotten so much harder. Fewer people understand, totally, what a given system does.
Fewer masters emerge, and the flow of knowledge is broken. This leads to mentorship gaps, and large institutions, be it universities or companies, rarely have a solution to this problem.
It sits unaddressed, and leaves everyone feeling some flavor of worthless. Crafting software becomes incredibly painful. And we often try to avoid pain.
We seek comfort in counterculture. I'd argue that Handmade is a form of counterculture: we're trying to ease the pain of practicing our craft.
It's comforting, because here, the growth and mastery curves align. It's so satisfying to see something work, while having a complete understanding of it. It's empowering!
And counterculture should be empowering. But we should take a critical view on the solutions that counterculture generates. Because there are no silver bullets, but there might be some really good alloys.
Often, we shrink the mountains, but lose the ecosystems that are thriving on their slopes. We lose things like office suites, web browsers, and the standards of interoperability that we've come to expect of the systems we build.
Mastery that's easy to obtain, but ineffective once obtained, sometimes stings. We're reminded that the 'real' mountains exist, and that we still need to climb them. It’s hard not to feel discouraged.
You’re still doing real, useful work, though. That never changes.
How can we make counterculture work? What if we switched things up and focused on cultivating intuition as the basis for a computing model?
What if we had our cake and ate it too? How can we give any individual, regardless of where they are in their computing journey, tools that are universal and easy to understand?
What would it look like? What form would it take? How far could we get?
Turns out, there exists a model that may check all the desirable boxes we’re concerned about. It’s beautifully symmetric, easy to implement, and provides a basis for a common language for communication of computing concepts.
It's called rewriting.
Much of my work in the past eight years has been focused around rewriting, and I've come to the conclusion that it is a perfect candidate solution for the problems that I’ve been talking about.
It takes many forms, and has many interpretations, but the basics are very easy to grasp.
It's also woefully under-explored and offers some very beneficial technical aspects, some of which are very hard to obtain in traditional languages and paradigms, and doing so is often a research topics in its own right.
In contrast, rewriting can be explained in a few minutes. It also offers a clear path to mastery, as there are very few moving parts, both implementation-wise and conceptually.
However, some developers seem to struggle a bit with it, trying to search for where the complexity, seemingly, got shifted to. We’ll cover that.
But first, I need to introduce you to the fundamentals.
There are two fundamental concepts in rewriting: structures, and rules. Structures are the 'medium' in which you encode your data, and there's typically just one, single, global structure to act on.
You can treat this global structure as your 'main memory', while rules are code that transforms a part of the structure in some way, guided by a form of pattern matching.
Everything we need can be built on top of these two concepts.
Structures can be any datastructure you're familiar with: strings, trees, sets, multisets, maps, etc. Rules are equally simple: they tell you what to search for in your structure of choice, and they tell you what to replace it with.
Rules always search for a sub-structure, or pattern, in the global structure. If you've ever used find and replace in your text editor of choice, you're using string rewriting rules!
We have a ton of ways to handle the matching of rules. I can pick a random rule from the ones that matched, pick the first rule that matched, or pick the rule with the most conditions that matched. We have a lot of options here!
I'd like to narrow my focus to multiset rewriting, as that's what I've been researching for the past few years. I’m also going to treat rules that match first in the rule list as ones that should 'win out' when we have multiple rules that match.
A multiset is a fancy name for a bag of things. Any bag of items is a multiset. Bags don't have any order to them, and can have duplicate items.
Turns out, these structures pop up everywhere, in places you don't really think about, and have some really, really useful properties!
Think about every random scribbling or unordered list you've ever written. Think hard, because a lot of things might qualify as unordered bags of items!
Grocery lists, to-do lists, ingredient lists. You'll start seeing them everywhere, eventually!
And not only that, several processes that occur naturally are able to be written in terms of rewriting.
The passing of time, the changing of the seasons, the cycle of crops, the act of cooking, the process of crafting and manufacturing.
Everything involves some before and after, cause and effect, things to consume and produce.
Which means that we can connect with a lot of baseline intuition that a lot of newcomers are already equipped with!
This is a simple rewrite rule. It searches the bag for 3 things: apples, oranges, and cherries. If it finds all of them, the rule will match, and we’ll remove apples, oranges and cherries from the bag.
We’ll then put a fruit salad back into the bag. We need all of the ingredients, all of the conditions, above the arrow, to match a rule.
If you can understand this, well done! You’ve seen your first rewriting program. This is our 'Hello, world!'.
We can have multiple rules. Here's a simple to-do list. The first rule searches the bag for a 'Do Chores' item, and if it finds one, it replaces it with 'Do Laundry', 'Do Dishes', and 'Sweep Floors'.
The second rule searches for a 'Laundry Done' item, a 'Dishes Done' item, a 'Floors Swept' item, and replaces them with a 'Chores Done' item.
It reads nicely, doesn't it? Here's the chores I have to do. Here's how I know all of my chores have been done. 'If I've done the laundry, washed the dishes, and swept the floors, I'm done with my chores.'
This looks familiar, doesn't it? A simple game loop. Now we're talking! This doesn't look like a rewriting rule, does it? It just looks like some kind of procedure, some kind of function definition.
That's not an accident: we get procedures for free with rewriting rules. They're a universal control flow structure! We can write in something that is, effectively, pseudocode, and have it mean what we intended it to mean.
By the way, this is a real piece of sample code! It’s a common loop I include in some rewriting programs that use graphics and input.
Let's get a bit weirder. What if the items in our bag were lists of words? What if we added the ability to add wildcards or variables to our patterns?
Well, we gain the ability to write more general rules! Here's a simple tool to help you navigate the weather, relative to your preferences. The rules are above, and the initial state of the bag is below. We start off with 3 items in the bag, telling us that it’s rainy outside, we don’t like rainy days, and we don’t like cold days.
The highlighted words represent 'holes' in the pattern: variables that can take on any value. What happens if we try to fire this rule?
Looks like this rule matched! We searched through the bag, found 'It's rainy outside', searched even further and found 'I don't like rainy days', and consumed them both!
We replaced them with 'I'll stay inside today', and because nothing else matched, we stopped!
Simple, right?
But how can we take this further, to something like game logic?
What if I have some complex information I want to throw into my bag? Say I'm writing a roguelike, and have some pieces of information that are relevant to my game.
I can write them in a very straight-forward manner, something that resembles note-taking rather than defining data. Here, I'm constructing a very small game world: one where entities exist at (X, Y) coordinates, can have weapons, and can get struck by lightning if they're holding a conductive weapon.
Notice the absence of things like classes, or types. I just wrote down what I knew I wanted in my game world. I conjured data from nothing.
And I can write some pretty straight-forward logic that handles those lightning strikes I want in my game world, including a general rule that looks up and applies the appropriate damage value.
This is pretty self-evident, right? I'm assembling what I need to do some useful work immediately as it’s required. I don’t have to care about classes, methods, attributes, types, or anything conceptually heavy.
What happens if we run these rules?
Looks like the enemy we defined earlier got struck by lightning, because they were carrying a sword, which is conductive. They were then hit for some damage after the strike.
It’s important to note the sequence of events here. Every time we successfully find a rule that matches, we 'fire' the rule, consuming everything above the arrow, and producing things below the arrow. We then go back to the first rule in the list, and start checking for matches from there.
So the first rule matched, which consumed the information about the lightning strike, and then the first rule failed to match. The second rule is able to match, and deals the damage to the enemy.
(
Video Demonstration)
It’s not like this is some kind of hypothetical system either. This is Grimoire, an interactive fiction system written by one of my team members, Yumaikas. Underneath, it’s utilizing rewriting rules to govern transitions in the story, relative to choices the player makes.
What’s really cool is how slim this is. Yumaikas had the idea and implemented it in a matter of hours. They were able to get off and running because rewriting lends itself well to small, simple interpreters and compilers.
If you’re familiar with Twine, this is incredibly similar. And it’s super cool! The source is also very, very small for what it does! The semantics compress incredibly well.
The representation of rewriting rules doesn’t need to be strictly tied to text, or any kind of specific encoding or language.
In fact, we can build programs in rewriting languages without having to touch text at all! We can use hand drawn symbols instead, making the resulting language pictographic!
And all of the magic is preserved, if not enhanced.
(
Video Demonstration)
This is an example Tote program. It depicts a farmer, tending their field according to the cycles of harvests. The rewriting rules model the passage of time, the growth of crops, the farmer’s harvest of the crops, and the growth of the farmer’s field over time.
This isn’t just an algorithm. This is describing a real world process! It doesn’t just describe a computation, it conveys an idea!
There’s some incredible power here. And it’s because of all of this beautiful symmetry that rewriting languages give you. I have never seen this in another model of computation. Ever.
(
Video Demonstration)
I don’t know about you, but when I saw that working, it was beyond magical. It was proof of life. It was like watching actual magic.
The tool also supports backwards and forward execution now. Tote was developed by Devine of Hundred Rabbits over the course of about a week after I described my plans for an IDE for rewriting.
Imagine that. An IDE in a week, running tic-tac-toe.
These are real programs.
They run on your computer.
They share the same underlying model of computation.
I can take a program I’ve written, import it into Tote, and experiment to my heart’s content.
I can export rules I’ve written from Tote to a textual format, and do crazy things like compile them to native code, or run them in an interpreter, or embed them in my application.
This is a fraction of what’s possible.
To show you what’s possible, I’d like to introduce you to Nova. Nova is a lightweight language for conversing with computers.
It is a programming language, a data format, a computing stack, and, I hope, the technical portion of the solution for the problems I’ve discussed today.
Nova is my attempt at an understandable computing System. It is intended to be conceptually light-weight, but flexible and malleable enough to fit into any situation where communication gaps exist.
Nova can serve your primary programming language, as a note format, as a method for conveying ideas, and as a sketching tool for software.
It is based on rewriting, and you’ve seen some examples of it earlier in the talk. It unifies several threads of research, some of which dates back to the 1920s, and I’m very excited to finally open up about these threads.
But first, a disclaimer. I am not here to promote my own language. I’m not here to just preach about how I have the solution to all of these problems sitting in a repository somewhere. I’m not here to start language wars.
I’m here to show you how rewriting can be applied effectively. I’m here to show you how I’m applying it to my project and what it can really do.
Rewriting is incredibly powerful. I’m just the messenger for that idea, because these concepts have been locked behind impenetrable walls for decades.
They deserve to be brought into the light.
Nova is a rewriting language. Nova performs rewriting over a bag of variable-length tuples. We call this bag the 'knowledge base', and we call the tuples in the bag 'facts'.
Like we covered earlier, Nova rules search for, and consume, items in the knowledge according to patterns. These patterns are simple and can contain variables that are bound to values when searching through the knowledge base.
Your only method for specifying any kind of data is by writing down facts, and your only method for writing any code is by writing rules that consume and produce facts.
Here are some example facts.
Let's shed some light on this.
The previous slide was example code. Simple, whitespace delimited tokens are all we really need to pull the magic off. It’s a simple touch, but because we’re not doing any kind of natural language parsing, it makes implementing Nova so much easier.
Maintaining the balance between easy and simple is key. I have tried to design it such that beginners can pick it up and run. Having a low amount of conceptual dependencies is crucial.
Newcomers don’t need to understand nesting, scopes, or any special cases or interactions between different language features.
It’s just rules and facts, flat and simple.
And while the implementation is simple, the leverage it gives you is much greater than you’d imagine. Things that would necessitate something like an ECS or a textbook on data-driven design and event sourcing take just a few lines in Nova.
I don’t have to think in terms of arrays, lists, or any structures that aren’t explicitly required to allow me to express my intent.
I can model complex logic, cross-cutting concerns, and corner cases that would be time consuming, and buggy, to model with traditional methods.
Let’s look at an example.
Let’s build a simple text adventure movement system, starting with some sample locations, and a starting location. Let’s also define some useful information about directions, like 'north is the opposite of south'.
A reminder, this is sample code. This is actual data that we can manipulate. This is our initial state of our program.
Let’s see what the code to handle movement looks like.
What do these two rules say?
If we’re in a place, and we move a direction, and there’s a place to move to that’s in that direction, we’re now standing in the adjacent place.
OR.
If we’re in a place, and we move a direction, and there’s a place to move that has a connection to the place I’m at, but in the opposite direction, we’re now standing in the adjacent place.
I only have to specify one connection between two places, and these rules will detect a move, and the connection, and move me around the game world. What happens if we run it?
Looks like the second rule was the only one that matched. Which is great! We’re in the foyer, we try to move east, the kitchen is west of the foyer, and west is the opposite of east, therefore there’s a connection between the foyer and the kitchen.
And because that rule fired, we’re now sitting in the kitchen. I can imagine the equivalent code in C, Python, or another language. It’s not that complex, but forces you to encode your thoughts into a new format that the computer can run.
Wouldn’t it be nice if we could just write what we meant?
As another example, here’s a little image of a smiley face, encoded as some Nova facts. We have X/Y coordinates, some RGB values, and an image name.
Let’s say, for example, I wanted to generate a palette from this image. I want a collection of all the colors in the image, and I want to give them unique names or identifiers so I can use them later.
What would the rules look like?
If I have a pixel in an image with some RGB value, and this RGB value is already in the palette, replace the RGB value with the name of the color in the palette.
If I have a pixel in an image with some RGB value, and nothing else, generate a brand new palette entry with that RGB value, and replace the RGB value at that pixel with the name of the color in the palette.
Notice, in the second rule, how “color” wasn’t featured on the left-hand side. If you use an unbound variable, we’ll just generate a unique value for you. This is incredibly useful, if not crucial, for things like games.
Just spawn a game entity, slap an ID on it and go!
How about some simple input handling? I’m pretty sure this is self-explanatory. I assume some rules exist that grab mouse input, do some rudimentary bounding box math, and handle some actions.
Super versatile, super simple, and can even act as a kind of configuration language that can either be incorporated into your build process, or into running code.
What does all of this buy you? Well.. rewriting languages model time in terms of rewrite steps, right? Find which rules match, apply rules, repeat. That means that if you stop the execution at any point, you can take the knowledge base, throw it on another computer, and resume execution.
Rewriting is natively reentrant, which buys you a ton of newfound power. Each rewriting step is like a database transaction: read, modify, write. It doesn’t matter if it’s in memory, on an SSD or spinning rust, or on another computer: everything is transparent to rewriting if you want it to be.
Each of these things, individually, is a research project in and of itself. But we get these properties for free with a simple model switch!
Nova can function as an independent language, or as a way to augment existing languages. I can take a piece of Nova code and compile it to assembly, C, Go, Java, JavaScript, you name it.
Imagine being able to hand someone a note file, integrate it into your build system, and collaborate with people that don’t use your particular programming language, but can now express computations and ideas in a common, portable format.
This is a huge win for things like game studios, small businesses, and people who just want to write simple, easy to understand tools for themselves that are self evident.
I didn’t design this as an educational language, but it fits the bill. With educational languages, there’s a common thread: you can’t carry them with you throughout your programming career.
MIT’s Scratch and Seymour Papert’s Logo can’t fit into the places we need them to without some heavy effort. Newcomers have to graduate to new, “real” languages to continue their journey. And I don’t think that’s fair to them.
We need to build tools that can evolve and persist as people grow and explore. If we don’t, we’re just setting them up for constant context switching, and giving them conceptual dependency graphs that rival some of the worst NPM packages.
We’re craftspeople. We need to start acting like them. We need to acknowledge our relation to other crafts and trades, and ask 'do we really have the right mindset?'
If we don’t, we’ll lose sight of the ground. We’ll lose sight of what it means to be a trade. We can’t keep this process of discarding skills and tools and methods up.
It’s just not tenable. Potters do not outgrow clay. It is crucial to their craft.
I’m really grateful to be presenting this at Handmade. I feel like we’re at a tipping point, where people are genuinely fed up with the status quo. We’re sick of seeing our craft exploited, our dreams grifted, and our hopes for the future dashed by another social problem peddling a technical solution.
And it’s here I feel the most difference can be made. Something has to change. And it’ll come in two parts, technical and social. Our craft will change. It has to, in order to survive. We have to become proper stewards from all angles.
With that, some closing thoughts.
Bret Victor’s presentations motivated me heavily when I was younger, but as I got older, I realized that he was using the same material as everyone else. He showed that we didn’t have to keep thinking of programs as opaque boxes that were constructed by roleplaying as a computer.
But even then, he was limited by things like JavaScipt. When you’re fighting your language, or your computing model, everything is incredibly hard. You are fighting the forces of nature and you will lose. The only solution is a change in model, in perspective, and in principle, usually all at once.
We have to find symmetry and balance in the things we build and use.
This work didn’t happen in a vacuum. I’ve spent the last 8 years digging up old papers and concepts, searching for fragmented pieces of knowledge via the Internet Archive, and trying to piece together the past that’s been forgotten. There’s so much research that didn’t get a capital infusion, and was left to die. Everything in this talk was an aggregation of that research.
And in that same spirit, gain some perspective and tear the scaffolding down around your programs. Get rid of unneeded structure, needless complexity. Try to connect with the people around you that aren’t doing what you’re doing. Think about how they’d understand what you’re working on, and if they couldn’t, simplify, simplify, simplify!
Doing more with less is one of the major principles I’ve been following, and will continue to follow. Ideas should be multi-function and cover as much ground as possible. This lets you breathe, and avoid falling into cognitive overload.
Don’t build factory factory factories. Don’t build things that you can’t physically relate to. Physical engineering has the benefit of having a limiter on the degrees of freedom of anything that’s built, enforced by things like gravity. We don’t have those kind of constraints.
Don’t focus on cramming as many concepts in your head as possible. Instead, ask 'I know X, so can I rephrase Y in terms of X?' every time you encounter a new concept. It’ll help you connect new concepts to old ones, and you’ll have an easier time learning.
Be careful about assuming what others know about what you’re working on. It’s easier to assume nobody knows what you’re working on, and learn to explain it in simple, but clear, terms. It’ll make you more confident, and you can practice it every day by talking to friends and family. Spread the joy of computing around.
And in doing so, you can build people up. You can tell them that they matter, and that it’s possible to have agency over their lives. That if a human understood this once, a human can understand this again. That mastery is achievable, and there’s a ton of other people that want to help them along, should they need it.
You can tell them that they aren’t alone.
Because we aren’t. We never were.
We just thought we were.
Thank you.
⦶
(
Tote, by Devine Lu Linvega)
(
Grimoire, by Yumaikas)
(
The Nova Wiki)
(
The Nova Forum)
(
The Nova Discord)
-------
(
back)
......
... .. ...
.. .. ..
.. .. ..
.. .. ..
.. .. ..
.. .. ..
... .. ...
......
wryl © 2025