Kolterdyx / mcbasic

A custom scripting language for generating complex Minecraft datapacks
GNU General Public License v3.0
0 stars 0 forks source link

While loops #2

Open Kolterdyx opened 4 months ago

Kolterdyx commented 4 months ago

In general, loops are not exactly possible with Minecraft functions. You could use a recursive function, but that will be limited to the maxCommandChainLength gamerule, which by default is 2^16 or 65536. There is no command equivalent to the JMP instruction commonly found in ASM instruction sets, so we can only execute stuff linearly. The only loop we have is the tick function, which runs since the world is loaded, forever, under all circumstances.

We can use that to create some sort of scheduling system. Functions that contain loops would be split up, with each loop being a "sub" function of the original one. Then we replace the while loop in the original function with some instruction that tells the scheduler to run a function until a condition is met.

We would have to create such a function so that it takes as parameters all the necesary variables from the parent scope, and the CALL counter should not be increased in loops. Then we could maybe add the name of this subfunction to a queue, and the scheduler would run the first function in the queue every tick until a condition was no longer met, and then it would remove the function from the queue, and move on to the next.

WOODEN-CHEST commented 4 months ago

Why not set the maxCommandChainLength to the highest value and then use recursive functions?

Kolterdyx commented 4 months ago

Because there is still a max value. That just delays the issue. If it takes too long to reach the exit condition, or if there is too much code inside the loop, the function will crash, which would be quite annoying imo

WOODEN-CHEST commented 4 months ago

Running 2,147,483,647 commands in a single tick is an huge amount and would freeze the game. It is hittable, but in normal and sane datapacks shouldn't happen.

Hitting the limit could just be a restriction of the language, which could be detected by setting a flag at the start of the tick command and unsetting it at the end, and testing its value at the start of each tick.

I think that trying to avoid the limit overcomplicates the implementation with little gains, as most users wouldn't hit the limit.

If the limit really needs to be bypassed, then it can be done by running multiple tick functions in a single tick from the tick function tag. For example, if the limit is set to 5 commands but two functions are ran from the tick tag, then 10 commands can be executed in each tick, because the limit is counted seperately for each tick function that minecraft calls. This could be used to run large loops in a single tick.

Kolterdyx commented 4 months ago

I just don't really like having a hard limit to how long a function can be, pecially considering that very few lines in MCBasic compile to tens of lines of actual minecraft commands. This results in compilation being unpredictable, as the user wouldn't be able to easily tell how many commands their MCBasic code is going to compile to. Even if we had a hard limit, it should be predictable and easily worked around by the user.

The scheduler, although more complex, would allow executing small sections of code one tick at a time, which would essentially spread the load across multiple game ticks. This would make stuff like nearly infinite loops possible, because they wouldn't be trying to run all iterations on the same tick, but just one each tick.

The scheduler would also allow us to split up sections that go over the limit and just chain them together, essentially allowing a function to compile to an unlimited amount of commands

Kolterdyx commented 4 months ago

Actually, the schedule command already exists, which simplifies things a lot lol. We just need to keep track of the stuff we have scheduled so we can clear it if we throw an exception

Kolterdyx commented 4 months ago

After some tinkering I have found that scheduling for later ticks will not be possible, as it will mess up the timings for when variables are set and when functions writing/reading them are called.

The only possible way would be to evaluate the source and find out how long it will take to run a snippet of code, and schedule the code after it to run at the appropiate time. This quickly turns into trying to solve the halting problem so I'm not even going to try. On second thought, it's not even all that necessary with the way minecraft datapacks are usually built.

In the end @WOODEN-CHEST 's solution will work better. Recursion is the way to go, and users should be careful and distribute the load between multiple functions with the tick tag

branyoto commented 2 weeks ago

An idea that I've implemented on one of my datapack is to batch the loop executions to reduce number of commands necessary to .

If you don't need the loop index or the target entity, you can do execute store success score COUNT loop as @e[limit=$(max)] run $(function) (might not be the correct syntax I'll check by tomorrow to be sure)

Then by storing the $(max) in a score, I can compute the difference and chose to recursively call the function or exit here.

This works really well when you have small loops or when there is many entities but in most cases this will reduce the number of commands and therefore increase the maximum of loop that you can do before reaching the maxCommandChainLength.

And finally to avoid loosing target entity context, you can tag it before the loop as the executor and run the function as him execute store success score COUNT loop as @e[limit=$(max)] run execute as @e[tag=executor] run $(function) (same I'll check later if this is the correct syntax)

branyoto commented 2 weeks ago

Full implementation of this context:

$scoreboard players set TMP loop $(max)
$execute as @e[limit=$(max)] summon area_effect_cloud run tag @s add loop_counter
$execute as @e[tag=loop_counter] run $(function)
execute store result score TMP2 loop run kill @e[tag=loop_counter]
scoreboard players operation TMP loop -= TMP2 loop
$data merge storage loop:counter {function: "$(function)"}
execute store result storage loop:counter max int 1 run scoreboard players get TMP loop
function namespace:count with storage loop:counter

Requirement: /scoreboard objectives add loop dummy Call /function namespace:count {max:17, function:"say hello you"}

The more entity you add the better it gets