Core Concepts
The ideas every OptiPine structure shares: three levels of control, who owns which array, the lifecycles that keep state correct, and the limits Pine sets underneath.
Three tiers
OptiPine is layered so that high-level code describes the problem rather than the mechanism. Each type's documentation states its tier.
| Tier | What it offers | Structures |
|---|---|---|
| 1, Quick | Ready-to-use calls with automatic defaults. | Watch, Memo, CadenceGate, typed stores, StablePool, DirtySet, row rings, double buffers, WeightedSampler |
| 2, Composable | Explicit lifecycles, configuration and representation policies. | IntIndex, IntBuckets, SlotCache, RingCursor, Revision, WeightedIndex, WeightedPrefix, FloatFenwick, IndexConfig, WeightConfig |
| 3, Expert | Physical addressing, unchecked writes and scoped raw mutation for measured hot paths. | Ring reserveRowUnchecked(), writeUnchecked(), writePhysical() and edit scopes; markUnchecked(); WeightedIndex setWeightUnchecked() and weight edit scopes |
Start with Tier 1. Most scripts never need anything else. Move a layer down only when a measured workload needs it, and keep Tier 3 calls inside the code path you measured.
Ownership and views
Every accessor makes one of three promises about the array it gives you:
- A value. Scalars such as
memo.get(),store.get(key)orring.at(row, column)return a value. Changing your copy changes nothing inside the structure. - A borrowed view. Calls such as
dirty.values(),pool.slots(),store.valuesView(),index.keysView(),ring.rowView()andring.rawView()return the structure's own array. It is read-only by contract and valid only until the structure changes. Do not keep it across bars, sort it or resize it. - Your own copy. The
*Copy()calls, such asdirty.valuesCopy()orcache.activeCopy(), and the calls that take anoutputarray return storage you own.
ring.chronological(output = na), index.findMany(queryKeys, output = na) or store.getMany(queryKeys, output = na) clear and refill an array you pass in, and only allocate a new one when you pass none. Keep a var output array and pass it every time.Why a view can break
A view is the live storage. pool.slots(), for example, removes a released slot by swapping it with the last one, and a pool keeps each slot's position in that list. Reordering the list from outside would desynchronize both. Reading it is always safe while nothing else changes the pool.
Lifecycles
Several structures work in passes or builds. Each has a fixed order of calls:
| Structure | Order | What it means |
|---|---|---|
| Watch | begin(), typed watch*() calls, finish() | One comparison pass over several dependencies. finish() reports whether the set changed. |
| Memo | staleOn(), or begin(), dependencies.watch*(), miss() | One-call check, or an explicit pass for collections and variable dependency lists. |
| DirtySet | begin() once per cycle, then mark() | A new cycle starts without clearing the universe. |
| SlotCache | begin(), acquire() each active key, finish() | One frame. Keys not acquired in it are retired. |
| IntIndex | buildBegin(), add() or addMany(), buildFinish() | The representation is chosen at buildFinish(). Lookups come after. |
| IntBuckets | buildFromPairs(), or buildBegin(), add(), buildFinish() | Every build replaces all previous groups. |
| Typed stores | build(keys, values, expectedQueries) | The whole build in one call. set() can change values but not the key set. |
| Row rings (Tier 3) | editBegin(), rawUnsafe(), editFinish() | Raw writes are allowed only inside the scope. editFinish() bumps the revision. |
IndexConfig, SlotPoolConfig and IntBuckets carry a strictLifecycle flag, on by default. Strict mode stops the script with a descriptive error on a call out of order. With it off, the same mistake degrades to a defined fallback instead, for scripts that must never halt.
Revisions
Row rings and typed stores keep a content revision, a counter that advances when their content changes. Writes that store the same value again leave it alone. That is what makes watch.changed(ring) O(1): the Watch compares one integer, not the ring's contents. For state OptiPine cannot see at all, a Revision token does the same job by hand.
What each call costs
| Structure | Cost |
|---|---|
| Watch | O(1) for one value or a ring; O(N) to compare an array, with the O(N) copy paid only when it changed. |
| Memo | O(number of dependencies) per check. |
| Row rings | push() is O(width); pushValue() and every single-value read or write are O(1). |
| StablePool | acquire(), release() and find() are O(1). |
| SlotCache | acquire() is O(1); finish() is O(slots active in the previous frame). |
| DirtySet | begin() and mark() are O(1); traversing values() costs the number of marks, never the universe. |
| IntIndex | find() is O(1) for dense and map, O(log n) for binary search, O(n) for a linear scan. |
| Double buffers | swap() is O(1) plus clearing the new current array. |
| WeightedSampler | setWeight() is O(1) with the prefix sum and O(log n) with a Fenwick tree. A draw is O(log n); with the prefix sum, the first draw after a weight change also rebuilds it in O(n). |
na and empty values
- Watch compares exactly.
naequalsna, and floats are compared without Pine's rounding. For arrays, NaCollectionPolicy decides whether annaelement is ordinary, always counts as a change, or raises. - Memo starts empty.
get()returns its fallback before the firststore():na, orfalsefor a BoolMemo.isValid()tells an empty memo from one holding the empty value. - Stores return an empty value on a miss. When a stored value may equal it, use
read(key), which returns[found, value], orcontains(key). - Keys are never na. Pools, indexes and buckets reject an
nakey with an error.
Limits underneath
OptiPine removes work around a calculation. It does not change what Pine allows:
- An array holds at most 100,000 elements, and a map at most 50,000 pairs.
- StablePool and SlotCache hold at most 50,000 live keys, because their key directory is a map.
- A ring's capacity times its width, and a DirtySet's universe, stay within 100,000.
- IntIndex holds up to 100,000 keys, or 50,000 when it uses a map. IntBuckets holds 100,000 pairs and 50,000 distinct keys.
- WeightedIndex and FloatFenwick hold at most 99,999 weights.
- A Revision wraps to 0 after 2,000,000,000 bumps.
The library exports two of these as constants: op.MAX_ARRAY_ELEMENTS and op.MAX_MAP_PAIRS.
Method names
Methods such as get(), set(), push() and clear() match Pine's own collection vocabulary on purpose. The Pine Editor may show a shadowing warning for them. It is cosmetic: the type of the object before the dot decides which method runs.