big revamp tilesets intro

This commit is contained in:
David Allemang
2026-07-29 12:28:56 -04:00
parent 0aa52320a3
commit bb71b7e872
2 changed files with 301 additions and 89 deletions

View File

@@ -1,17 +1,19 @@
#import "/lib.typ": diorama, example, note, todo
#show raw.where(lang: "mc-diorama"): it => diorama(autoplay: true, loop: true, theta: 160, phi: 20, it.text)
= Tilesets <tilesets>
The protocols as described are very sensitive to the exact timing that the EID counter is
incremented. The fundamental cause for all types of interference is that we spawn a reference and
measurement item with too much delay, and some unexpected entity (or entities) spawn in-between. To
mitigate interference, then, we need that window to be *as short as possible* while still allowing
us to spawn transmission entities in that window.
incremented. The fundamental cause for all types of interference is that there is a chance for
unexpected entities to spawn in-between the measurement items, which increments the counter and
corrupts the data. The solution, then, is to make that window *as short as possible* so the chance
for interference is minimized.
We use the tile tick priority queue to define a global order of sub-tick execution.
The fundamental principle is to use the game scheduled tick priority queue to do this. A *tile
sequence* is a chain of redstone components that all update in the scheduled tick phase. A *tile
set* is a system of constructing tile sequences that enforces a particular global ordering.
#show raw.where(lang: "mc-diorama"): it => diorama(autoplay: true, loop: true, theta: 160, phi: 20, it.text)
```mc-diorama
p 0 -1 0 smooth_stone_slab type=top
p 1 -1 0 smooth_stone_slab type=top
@@ -45,112 +47,241 @@ t 20
p 0 0 0
```
#todo[fix tileset naming. the tileset is the *structure*. the particular sequence is just one instance of that tileset]
== The Scheduled Tick Priority Queue
A *tile sequence* is a chain of redstone components that all update in the tile tick phase.
The fundamental principle here is to use tile-tick priority (TTP) to set a global order.
#link("https://youtu.be/sLftwVwqPQE")[See Charlie's great video on TTP for details].
There are two aspects that affect a scheduled tick's placement in the queue: its _priority_ and its
_delay_. Scheduled ticks with higher (more negative) priority update first in a given game tick.
Scheduled ticks with equal priority update in the order they were scheduled.
A *tile set* is a system of constructing tile sequences that enforces a particular global ordering.
See @block-priority for a list of priorities. The most important point is that repeaters have higher
priority than other components.
== Binary Tilesets
The simplest (and generally most useful) tilesets use only two priorities. This allows us to
identify each tile sequence with a binary value and easily read the update order and carve up the
the full tile set for different systems. Typically we use comparators (or observers) and 2gt
repeaters.
We identify the higher priority (repeater) with `0`, since it sorts/updates earlier, and identify
the lower priority (observer/comparator) with `1`. Then we can read off the elements of the tile
sequence to form a binary number.
Components that activate later in the sequence have a stronger effect on the order, so to match our
left-to-right reading convention, we diagram tile sequences so the signal flows right-to-left.
== The Tile Tick Priority Queue
Consider this 4gt tileset.
The game schedules tile updates through a priority queue; different components have different
priority, so each stage of the tileset iteratively refines the global order. Across the full
tileset, we can enforce an arbitrary global ordering.
#show raw.where(lang: "mc-diorama"): it => diorama(
autoplay: true,
loop: true,
theta: 90,
phi: 90,
speed: 0.33,
radius: 8,
orbit: false,
it.text,
)
#todo[the priority table][
call out here that basic comparators and all other components, so from here on out we're just
going to use comparators for simplicity.
]
```mc-diorama
p 0 -1 0 smooth_stone_slab type=top
p 1 -1 0 smooth_stone_slab type=top
p 2 -1 0 smooth_stone_slab type=top
p 3 -1 0 smooth_stone_slab type=top
p 4 -1 0 smooth_stone_slab type=top
p 5 -1 0 smooth_stone_slab type=top
p 6 -1 0 smooth_stone_slab type=top
p 7 -1 0 smooth_stone_slab type=top
Components with different priority always update in priority order, but components with equal
priority update in scheduled order.
p 0 0 0 redstone_wire east=side west=none south=side power=0
p 1 0 0 redstone_wire east=side west=side south=side power=0
p 2 0 0 redstone_wire east=side west=side south=side power=0
p 3 0 0 redstone_wire east=side west=side south=side power=0
p 4 0 0 redstone_wire east=side west=side south=side power=0
p 5 0 0 redstone_wire east=side west=side south=side power=0
p 6 0 0 redstone_wire east=side west=side south=side power=0
p 7 0 0 redstone_wire east=none west=side south=side power=0
#todo[rephrase or remove][
The core principal is that repeaters have higher priority than other components. For example, if a
repeater and comparator are scheduled to activate in the same tick, the repeater always (with one
exception, if the comparator faces into a diode and the repeater does not) activates before the
comparator does. By choosing a regular building pattern, we can avoid this exception entirely.
p 0 0 1 repeater facing=north delay=1 powered=false locked=false
p 1 0 1 comparator facing=north mode=compare powered=false
p 2 0 1 repeater facing=north delay=1 powered=false locked=false
p 3 0 1 comparator facing=north mode=compare powered=false
p 4 0 1 repeater facing=north delay=1 powered=false locked=false
p 5 0 1 comparator facing=north mode=compare powered=false
p 6 0 1 repeater facing=north delay=1 powered=false locked=false
p 7 0 1 comparator facing=north mode=compare powered=false
// Among repeaters, they activate in the order in which they were scheduled. And among
// comparators, they activate in the order in which they are scheduled. But every repeater
// activates before any comparator (aside from that one exception). This gives us a *stable sort*
// which we can use to globally define update order with arbitrary precision.
p 0 0 2 repeater facing=north delay=1 powered=false locked=false
p 1 0 2 repeater facing=north delay=1 powered=false locked=false
p 2 0 2 comparator facing=north mode=compare powered=false
p 3 0 2 comparator facing=north mode=compare powered=false
p 4 0 2 repeater facing=north delay=1 powered=false locked=false
p 5 0 2 repeater facing=north delay=1 powered=false locked=false
p 6 0 2 comparator facing=north mode=compare powered=false
p 7 0 2 comparator facing=north mode=compare powered=false
]
p 0 0 3 repeater facing=north delay=1 powered=false locked=false
p 1 0 3 repeater facing=north delay=1 powered=false locked=false
p 2 0 3 repeater facing=north delay=1 powered=false locked=false
p 3 0 3 repeater facing=north delay=1 powered=false locked=false
p 4 0 3 comparator facing=north mode=compare powered=false
p 5 0 3 comparator facing=north mode=compare powered=false
p 6 0 3 comparator facing=north mode=compare powered=false
p 7 0 3 comparator facing=north mode=compare powered=false
#example[
#todo[this explanation is so janky]
t 0
p 0 0 0 power=15
p 1 0 0 power=15
p 2 0 0 power=15
p 3 0 0 power=15
p 4 0 0 power=15
p 5 0 0 power=15
p 6 0 0 power=15
p 7 0 0 power=15
Say these three tilesets are started in the same gametick, `t=0`.
t 1
p 0 0 1 powered=true
p 2 0 1 powered=true
p 4 0 1 powered=true
p 6 0 1 powered=true
```
<------
1 rep cmp ab
2 cmp rep cd
3 2rep e
<------
```
t 2
p 1 0 1 powered=true
p 3 0 1 powered=true
p 5 0 1 powered=true
p 7 0 1 powered=true
Once `b`, `d`, and `e` are scheduled, the priority queue looks like this:
t 3
p 0 0 0 power=0
p 1 0 0 power=0
p 2 0 0 power=0
p 3 0 0 power=0
p 4 0 0 power=0
p 5 0 0 power=0
p 6 0 0 power=0
p 7 0 0 power=0
```
t=2 [d] [b]
t=4 [e]
```
t 4
p 0 0 2 powered=true
p 4 0 2 powered=true
t 5
p 1 0 2 powered=true
p 5 0 2 powered=true
t 6
p 2 0 2 powered=true
p 6 0 2 powered=true
t 7
p 3 0 2 powered=true
p 7 0 2 powered=true
Now at `t=2`, we process the queue in order.
t 8
p 0 0 1 powered=false
p 2 0 1 powered=false
p 4 0 1 powered=false
p 6 0 1 powered=false
- `d` activates and schedules `c`.
- `b` activates and schedules `a`.
t 9
p 0 0 3 powered=true
t 10
p 1 0 3 powered=true
t 11
p 2 0 3 powered=true
t 12
p 3 0 3 powered=true
```
t=4 [e a] [c]
```
t 13
p 1 0 1 powered=false
p 3 0 1 powered=false
p 5 0 1 powered=false
p 7 0 1 powered=false
Now at `t=4`, the end of the tileset, the lanes will always update in order `3 1 2`
t 14
p 4 0 3 powered=true
t 15
p 5 0 3 powered=true
t 16
p 6 0 3 powered=true
t 17
p 7 0 3 powered=true
]
t 18
p 0 0 2 powered=false
p 4 0 2 powered=false
t 19
p 1 0 2 powered=false
p 5 0 2 powered=false
t 20
p 2 0 2 powered=false
p 6 0 2 powered=false
t 21
p 3 0 2 powered=false
p 7 0 2 powered=false
So the full picture involves arbitrary components and priorities. As long as all the tilesets have
the same total delay and end at the same time, we can determine a global ordering. However the
general picture is hard to reason about, we basically have to simulate the priority queue to make
predictions. If we restrict the design of the tilesets a bit, there are two simplifications we could
take to make things easier to reason about.
t 22
p 0 0 3 powered=false
t 23
p 1 0 3 powered=false
t 24
p 2 0 3 powered=false
t 25
p 3 0 3 powered=false
t 26
p 4 0 3 powered=false
t 27
p 5 0 3 powered=false
t 28
p 6 0 3 powered=false
t 29
p 7 0 3 powered=false
== Permutation Tilesets
#todo[describe the mixed delay permutation tilesets and the alphabetization procedure]
== Binary Tilesets
The simplest tilesets are made entirely of comparators and 2gt repeaters (alternatively: observers
and 2gt repeaters). Repeaters activate before comparators, so if we think of it like sorting words
alphabetically, we can identify repeaters with "A" and comparators with "B".
So, suppose we have a "binary tileset" that is 2gt long. There are two options, A and B. If we
alphabetize these, we see the A (repeater) always executes before the B (comparator). With such a
short tileset, that seems trivial; things get more interesting as we add more elements.
```
<--
cmp (B)
rep (A)
t 30
p 0 0 0
```
Now let's use 2 components for a 4gt tileset. There are now four options, AA, AB, BA, BB. We can
alphabetize these and see the order.
We can express the same tileset numerically like so:
```
<------
cmp cmp (BB)
cmp rep (BA)
rep cmp (AB)
rep rep (AA)
000 = 0
001 = 1
010 = 2
011 = 3
100 = 4
101 = 5
110 = 6
111 = 7
```
Using the same pattern, we can generate a tile sequence for an arbitrary value of arbitrary
bit-length. For example, the 8-bit tileset for `150` is:
10010110
#show raw.where(lang: "mc-diorama"): it => diorama(
theta: 90,
phi: 90,
speed: 0.33,
radius: 5,
orbit: false,
it.text,
)
```mc-diorama
p 0 -1 0 smooth_stone_slab type=top
p 0 -1 1 smooth_stone_slab type=top
p 0 -1 2 smooth_stone_slab type=top
p 0 -1 3 smooth_stone_slab type=top
p 0 -1 4 smooth_stone_slab type=top
p 0 -1 5 smooth_stone_slab type=top
p 0 -1 6 smooth_stone_slab type=top
p 0 -1 7 smooth_stone_slab type=top
p 0 0 0 repeater facing=north powered=false locked=false delay=1
p 0 0 1 comparator facing=north powered=false mode=compare
p 0 0 2 comparator facing=north powered=false mode=compare
p 0 0 3 repeater facing=north powered=false locked=false delay=1
p 0 0 4 comparator facing=north powered=false mode=compare
p 0 0 5 repeater facing=north powered=false locked=false delay=1
p 0 0 6 repeater facing=north powered=false locked=false delay=1
p 0 0 7 comparator facing=north powered=false mode=compare
```
To break this down: the bottom two (AA, AB) end in repeaters, so they must come first. Among those,
@@ -197,6 +328,81 @@ next lane by simply incrementing by one.
cmp rep rep cmp cmp rep rep
```
// So the full picture involves arbitrary components and priorities. As long as all the tilesets
// have
// the same total delay and end at the same time, we can determine a global ordering. However the
// general picture is hard to reason about, we basically have to simulate the priority queue to make
// predictions. If we restrict the design of the tilesets a bit, there are two simplifications we could
take to make things easier to reason about.
== Permutation Tilesets
#todo[describe the mixed delay permutation tilesets and the alphabetization procedure]
== Lexicographic Tilesets
== Jamming
== Observer Bug <observer-bug>
== Tables
=== Block Tick Priorities <block-priority>
#table(
columns: 4,
table.cell(rowspan: 3)[Repeater],
[Facing diode (that is not facing it)],
[`EXTREMELY_HIGH`],
[-3],
[Powering off],
[`VERY_HIGH`],
[-2],
[Powering on],
[`HIGH`],
[-1],
table.cell(rowspan: 2)[Comparator],
[Facing diode (that is not facing it)],
[`HIGH`],
[-1],
[Otherwise],
[`NORMAL`],
[0],
table.cell(colspan: 2)[All other components],
[`NORMAL`],
[0],
)
=== Block Tick Delays <block-delay>
#table(
columns: 2,
[Repeater], [2gt, 4gt, 6gt, 8gt],
[Comparator], table.cell(rowspan: 4)[2gt],
[Redstone Torch],
[Falling Blocks],
[Observer (see @observer-bug)],
[Dispenser], table.cell(rowspan: 3)[4gt],
[Dropper],
[Redstone Lamp],
[Iron Pressure Plate], table.cell(rowspan: 4)[10gt],
[Gold Pressure Plate],
[Tripwire Hook],
[Tripwire],
[Stone Button], table.cell(rowspan: 3)[20gt],
[Stone Pressure Plate],
[Detector Rail],
[Wooden Button], table.cell(rowspan: 2)[30gt],
[Wooden Pressure Plate],
)
=== Fluid Tick Delays <fluid-delay>
#table(
columns: 2,
[Water], [5gt],
[Nether Lava], [10gt],
[Lava], [30gt],
)

View File

@@ -17,7 +17,6 @@ body {
font: 1rem / 1.5 sans-serif;
}
.tip {
color: forestgreen;
}
@@ -56,13 +55,11 @@ pre code {
text-wrap: wrap;
}
.callout, details {
.callout, details, table td {
margin: 1em 0;
padding: 0.5em 1ch;
background: rgb(from currentColor r g b / 5%);
border: solid rgb(from currentColor r g b / 30%);
border-width: 1px;
border-style: outset;
border: 1px outset rgb(from currentColor r g b / 30%);
}
.callout {
@@ -85,4 +82,13 @@ pre code {
summary::marker {
content: "Click to show ";
padding-left: 0;
}
table {
border-collapse: collapse;
width: 100%;
td {
vertical-align: text-top;
}
}