As discussed in the previous post, I proceeded with changing the liquids over to use the new "block-defined byte" aspect for flow distance, instead of dedicated block types. While this was a lot of ugly work (you can no longer just read a block type to interpret a liquid), it did end up removing a bunch of block types and opened the door to creating different liquid flow distances in the future.
While looking at doing the same thing to plants, I realized that it isn't quite that simple due to item drops when a block is broken. This is important since an immature plant should not drop final plant products. While the code to plumb through the extra parameter wouldn't be too bad, as this is only called in a few places and always from the top-level, I realized that maybe there is a better way.
I am now wondering if, instead of reducing all plant growth stages to 1 block per plant type, I reduce it to 2: Mature and immature? This would mean the block item drops could remain as they are but it would also make some other things a bit more elegant.
Specifically, the periodic callbacks. Those currently use internal imperative logic to decide whether or not to reschedule themselves and the only one which ever says "no" is the plant case, and only matters in the case of the plant becoming mature (or a tree, but that case decays into the idea I have, anyway).
This means that I could make the periodic callback scheduling logic a purely declarative mechanism, and could then derive it purely from the block type, alone.
This would also mean it starts to look a lot like a generalized "hook mutations to take actions" instead of "run a bunch of checks to decide which actions to take" system. In fact, together, these start to fit together naturally: A block being set triggers a callback based on block type which then requests the scheduling of periodic events, also based on block type. The eventual idea is to string together more of these cases into just event-based callbacks (or lists of callbacks, for composition) instead of inline imperative logic. This might even perform better since it changes a bunch of reachy if statements into a single invokeinterface, no matter the number of checks.
While this indirection makes debugging and reasoning about specific cases more complicated, live debugging it is not much different. However, understanding the entire system then becomes much simpler.
This fits nicely into my general philosophy that declarative logic is always preferable to imperative, except when it is done gratuitously, done in hidden ways, or otherwise feels "forced". In this case, it feels as though it is evolving naturally out of the existing complexity of a large imperative system.
Since this current release has no specific high-level goals, it seems like the right time to explore this evolution,
Jeff.