Views would invoke c->Exit() upon closing themselves, which would close them but not delete them. The controller would then be destroyed, fail to close the view because it wouldn't be the window on top, and skip destroying it in fear of something worse. This behaviour was introduced by c2f8a7df25f3, fixing something indeed worse: views would be destroyed while they were still in use by ui::Engine, sitting somewhere on the window stack but not at the top. In other words, the problem was (and is) that the terms of ownership of views between controllers and ui::Engine are unclear.
This commit effectively undoes that earlier commit and tries a different approach: when something is closed, close everything above it. This seems a correct thing to do and also allows controllers to unconditionally take care of their views. Prepare your popcorn though.
This fixes a bug where the stamp browser would stop rendering stamps if it was previously closed "too quickly", which turned out to mean that it was closed before all stamps had finished rendering, and thus ThumbnailRendererTasks belonging to LocalBrowserViews indirectly through SaveButtons would get stuck in a queued state, preventing other SaveButtons from starting their own ThumbnailRendererTasks.
Namely, when any dimension of the underlying particle area is divisible by CELL. Importantly, this includes "normal" saves (as opposed to stamps), which take the entire simulation area, making them impossible to load.
Broken by c2bb77721208, where RectBetween(topLeft / CELL, bottomRight / CELL) was rewritten to RectSized(pos / CELL, size / CELL), which is not equivalent. This commit also neglected to add TopLeft() and BottomRight() to Rect. Incorrectly fixed by 6a903ed132b3, where RectSized(pos / CELL, size / CELL) was rewritten to RectBetween(pos / CELL, (pos + size) / CELL), which is also not equivalent.
Should fix dialog coming up when typing / on some Brazilian keyboards, which use Alt Gr + q to type a /. Apparently this also acts as if Ctrl were held.
The save's blockSize was set one cell too short, so extend it by one (by using RectBetween). This fixes a crash when serializing save data that had particles outside its range.
This is fine, there's no particular benefit to hiding it when they are focused. I did find it annoying that the cursor got a bit tangled up with the placeholder text though, so I moved the text a bit to the right.
This leaves one non-C++ TU in the entire codebase: Cocoa.mm is Objective C++ and is only compiled on MacOS, where it's used for clipboard integration. This too will go away when we move to SDL3, which has a proper clipboard API.
Namely, when those rectangles are not drawn in a top left to bottom right corner order. Broken by a26544ba955f, which neglected to port the rectangle normalization logic from Simulation::ToolBox.
This makes the task of deciding which tool goes in which menu easier (GameModel::BuildMenus). Due to changes in the order tools are added to menus, the sign/sample/prop tool triplet now comes before custom tools, and the custom life creation tool comes before custom life element tools, which I think is fine.
This mimics sim.walls.* rather than elem.*_PT_*. The latter is fine because element identifiers follow the *_PT_* pattern, but tool identifiers don't, so they'd be a pain to tell apart from the rest of the API.
This was because element tools associated with Lua elements were reallocated every time their properties changed. They are now properly updated instead.
This is still a mess and I don't like it at all.
Don't just set the .type of struct Particle to 0, kids. Code tracked back to the times from before git; already present in b0ea52690ba5, the initial commit.
Reproduce with
sim.clearSim()
elem.property(elem.DEFAULT_PT_DMND, "Update", function(i, x, y)
local ni = sim.photons(x, y)
if ni then
if not sim.partProperty(ni, "vx") then
print(x, y)
assert(false)
end
end
end)
local i = sim.partCreate(-1, 100, 100, elem.DEFAULT_PT_NEUT)
sim.partProperty(i, "vx", 0)
sim.partProperty(i, "vy", 2)
sim.partCreate(-1, 100, 100, elem.DEFAULT_PT_DMND)
sim.partCreate(-1, 100, 102, elem.DEFAULT_PT_PRTI)
sim.paused(true)
sim.framerender(1)
Though if done this way, it's actually a photons corruption.
Render mode checkboxes each control multiple bit flags, but they would fully unset all bits they control, even if they shared flags with other render modes (which they always do). This would cause more things to turn off than intended when deselecting options, and all other checkboxes would appear unchecked. Now it will recalculate render mode based on what's checked instead.
Air display mode checkboxes are supposed to act as radio buttons, but it was letting all air modes be selected at once. They are mutually exclusive because only one display background can be drawn at once. Now, they act as exclusive radio buttons again.
That is, it's not thrown away and rebuilt every time the underlying set of tools changes (the "derivation" approach). Rather, changes to the underlying set are applied to GameModel's list (the "persistent" approach).
Normally, I'd prefer the derivation approach if its overhead is small enough, which in this case it would be. However, this change, among a few others, is done in preparation for exposing tool functionality to Lua. The Lua side will use persistent numeric identifiers to refer to tools, which means at least Lua tools would need to follow the persistent approach, at which point it makes more sense for all tools to do so, than a hybrid approach.
Also uncurse WindTool in the process; it is now an ordinary SimTool. This required extending SimTool with a Drag method that is called every tick while the user prepares for drawing a line and is shown the preview.
Since the Simulation doesn't have to understand SimTools anymore, they don't have to be a part of SimulationData and so NoToolClasses.cpp can finally be removed from the renderer executable.
Somehow foundElements (now renamed to foundParticles) made it into RendererSettings in 820f44a71690, even though it's actually a result of the rendering process. It is now extracted the same way as the resulting frame.
The default directory is acquired via getExternalFilesDir. Typical value: /storage/emulated/0/Android/data/uk.co.powdertoy.tpt/files, which seems to be user-accessible both on old and new Android. Probably. Hopefully. We'll see.
I tried to document everything the advanced search does, but it was mostly a guessing game discovering all its features. It appears to be insanely powerful, supporting AND, OR, negation, parenthesis,quote-encapsulated phrases, and limiting search to only certain fields. These can be chained together as well. I tried giving examples that could possibly be useful.
Terms like before: and after: are not part of advanced search, and thus are global search options even if they're inside parenthesis
That is, only if the Renderer needs to immediately blast off to render another frame. If we run it on the main thread, we can just use its video buffer directly.
"Possible" here means "when there are no particles with Lua graphics functions that haven't reported that they don't want to be called again and there are no beforesimdraw/aftersimdraw event handlers". I put my trust in Simulation::elementCount here; I sure hope it's always correct. If there is any of these, the renderer thread is paused and rendering happens on the main thread. To be clear, these are the situations in which Lua code is called with LuaScriptInterface::eventTraits having the eventTraitSimGraphics bit set.
If the detection of this situation is somehow flawed, there will still be no crashes because when running on the renderer thread, Renderer is told to not call Lua functions, and GameController::BeforeSimDraw/AfterSimDraw is not called by GameView::RendererThread. The worst that can happen is that some particles are not rendered properly.
Renderer settings (color and display modes, deco state, finding-element state, etc.) are managed by GameModel in the form of a RendererSettings and are passed to Renderer before each frame. The RenderableSimulation that Renderer works off of is also configured before each frame, either to point to a copy dedicated to the renderer thread, or directly to GameModel's Simulation, if the renderer thread is paused. Similarly, the result of the rendering is managed by GameView in the form of a RendererFrame, to enable e.g. sampling with deco tools without having to pause the renderer thread.
Each time GameView::OnDraw is called, it checks whether rendering is allowed to happen on a different thread (see above for conditions).
If it is, but the renderer thread is absent, GameView starts it and dispatches it (provides with settings and simulation data) right away. At this point, the renderer thread is definitely rendering a frame. GameView waits for this to finish and exchanges data with the renderer once it's done: it takes the result of the rendering and also dispatches the renderer thread again. This introduces a one-frame delay in rendering, which we can live with.
If rendering is not allowed to happen on a different thread, GameView waits for the renderer thread to finish what it's currently doing and pauses it, then proceeds to render the frame by itself.
Affecting this condition (whether rendering is allowed to happen on a different) by installing or removing event handlers, or clearing graphics cache (which, note, requires acquiring a unique lock on it), while the renderer thread is working, is not an issue because the renderer thread doesn't bother with event handlers, and because it acquires a shared lock on the graphics cache for rendering.
The GameModel's Renderer is thus still primarily managed by GameView, potentially in two different threads, in a strictly non-overlapping manner. The exception to this is when RenderView butts in and starts doing renders of its own; in such cases, the renderer thread must be paused explicitly, as RenderView does by calling GameView::PauseRendererThread.