<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/util/ReadWriteBlockingQueueTest.java<|end_filename|>
/**
 * Copyright 2020 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import org.junit.Assert;
import org.junit.Test;

import java.util.concurrent.CountDownLatch;

public class ReadWriteBlockingQueueTest {
	private static final int MAX_CAPACITY = 5;

	@Test
	public void testPut() {
		final ReadWriteBlockingQueue<String> queue = new ReadWriteBlockingQueue<>(MAX_CAPACITY);
		for(int i = 0; i < MAX_CAPACITY; i++) {
			queue.offer("test" + i);
		}

		final Thread thread1 = new Thread(() -> {
			for(int i = 0; i < MAX_CAPACITY; i++) {
				try {
					queue.put("test" + (MAX_CAPACITY + i));
				} catch (InterruptedException e) {}
			}
		});

		final Thread thread2 = new Thread(() -> {
			for(int i = 0; i < MAX_CAPACITY; i++) {
				queue.poll();
			}
		});

		thread1.start();
		Assert.assertEquals(MAX_CAPACITY, queue.size());

		try {
			Thread.sleep(250);
		} catch (Exception e) {}

		Assert.assertEquals(MAX_CAPACITY, queue.size());
		thread2.start();

		try {
			thread1.join();
			thread2.join();
		} catch (Exception e) {}

		Assert.assertEquals(MAX_CAPACITY, queue.size());
	}

	@Test
	public void testTake() {
		final ReadWriteBlockingQueue<String> queue = new ReadWriteBlockingQueue<>(MAX_CAPACITY);
		final Thread thread1 = new Thread(() -> {
			for(int i = 0; i < MAX_CAPACITY; i++) {
				try {
					queue.take();
				} catch (InterruptedException e) {}
			}
		});
		thread1.start();
		Assert.assertEquals(0, queue.size());

		try {
			Thread.sleep(100);
		} catch (Exception e) {}

		final Thread thread2 = new Thread(() -> {
			for(int i = 0; i < MAX_CAPACITY; i++) {
				queue.offer("test" + i);

				try {
					Thread.sleep(10);
				} catch (Exception e) {}
			}
		});

		thread2.start();
		try {
			thread1.join();
			thread2.join();
		} catch (Exception e) {}

		Assert.assertEquals(0, queue.size());
	}

	@Test
	public void testMultiplePutTake() {
		final int totalThreads = 8;
		final Thread[] threads = new Thread[totalThreads];
		final ReadWriteBlockingQueue<String> queue = new ReadWriteBlockingQueue<>(MAX_CAPACITY);

		for(int i = 0; i < totalThreads; i++) {
			if(i % 2 == 0) {
				threads[i] = new Thread(() -> {
					for(int ii = 0; ii < 100; ii++) {
						try {
							queue.put("test" + ii);
						} catch (InterruptedException e) {}
					}
				});
			} else {
				threads[i] = new Thread(() -> {
					for(int ii = 0; ii < 100; ii++) {
						try {
							queue.take();
						} catch (InterruptedException e) {}
					}
				});
			}
		}

		for(int i = 0; i < totalThreads; i++) {
			threads[i].start();
		}
		for(int i = 0; i < totalThreads; i++) {
			try {
				threads[i].join();
			} catch (Exception e) {}
		}

		Assert.assertEquals(0, queue.size());
	}
}


<|start_filename|>kotlin/src/main/java/org/mini2Dx/miniscript/kotlin/KotlinScriptExecutor.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2017 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.kotlin;

import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineBase.CompiledKotlinScript;
import org.jetbrains.kotlin.ir.expressions.IrConstKind.Char;
import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngine;
import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory;
import org.jetbrains.kotlin.util.KotlinFrontEndException;
import org.mini2Dx.miniscript.core.*;
import org.mini2Dx.miniscript.core.exception.ScriptSkippedException;

/**
 * An implementation of {@link ScriptExecutor} for Kotlin-based scripts
 */
public class KotlinScriptExecutor implements ScriptExecutor<CompiledKotlinScript> {
	private final KotlinScriptExecutorPool executorPool;
	private final KotlinJsr223JvmLocalScriptEngine engine;

	public KotlinScriptExecutor(KotlinScriptExecutorPool executorPool) {
		this.executorPool = executorPool;

		engine = (KotlinJsr223JvmLocalScriptEngine) new KotlinJsr223JvmLocalScriptEngineFactory().getScriptEngine();
	}

	@Override
	public GameScript<CompiledKotlinScript> compile(String script) {
		return new PerThreadGameScript<>(script);
	}

	@Override
	public ScriptExecutionResult execute(int scriptId, GameScript<CompiledKotlinScript> script, ScriptBindings bindings,
			boolean returnResult) throws Exception {
		final PerThreadGameScript<CompiledKotlinScript> threadScript = (PerThreadGameScript<CompiledKotlinScript>) script;

		final KotlinEmbeddedScriptInvoker embeddedScriptInvoker = executorPool.getEmbeddedScriptInvokerPool().allocate();
		embeddedScriptInvoker.setScriptBindings(bindings);
		embeddedScriptInvoker.setScriptExecutor(this);
		embeddedScriptInvoker.setParentScriptId(scriptId);

		for (String variableName : bindings.keySet()) {
			engine.put(variableName, bindings.get(variableName));
		}
		engine.put(ScriptBindings.SCRIPT_PARENT_ID_VAR, -1);
		engine.put(ScriptBindings.SCRIPT_ID_VAR, scriptId);
		engine.put(ScriptBindings.SCRIPT_INVOKE_VAR, embeddedScriptInvoker);

		try {
			engine.eval(threadScript.getContent());
		} catch (Exception e) {
			if(e.getMessage().contains(ScriptSkippedException.class.getName())) {
				throw new ScriptSkippedException();
			} else if(e.getCause() instanceof ScriptSkippedException) {
				throw new ScriptSkippedException();
			} else {
				throw e;
			}
		}

		executorPool.getEmbeddedScriptInvokerPool().release(embeddedScriptInvoker);

		if (!returnResult) {
			return null;
		}

		ScriptExecutionResult executionResult = new ScriptExecutionResult(null);
		for (String variableName : bindings.keySet()) {
			putResult(executionResult, variableName);
		}
		putResult(executionResult, ScriptBindings.SCRIPT_ID_VAR);

		return executionResult;
	}

	@Override
	public void executeEmbedded(int parentScriptId, int scriptId, GameScript<CompiledKotlinScript> script,
								EmbeddedScriptInvoker embeddedScriptInvoker, ScriptBindings bindings) throws Exception {
		throw new RuntimeException("Embedded synchronous script invokes not supported in Kotlin.");
/*		PerThreadGameScript<CompiledKotlinScript> threadScript = (PerThreadGameScript<CompiledKotlinScript>) script;
		engine.put(ScriptBindings.SCRIPT_PARENT_ID_VAR, parentScriptId);
		engine.put(ScriptBindings.SCRIPT_ID_VAR, scriptId);
		embeddedScriptInvoker.setParentScriptId(scriptId);

		try {
			engine.eval(threadScript.getContent());
		} catch (Exception e) {
			if(e.getMessage().contains(ScriptSkippedException.class.getName())) {
				throw new ScriptSkippedException();
			} else if(e.getCause() instanceof ScriptSkippedException) {
				throw new ScriptSkippedException();
			} else {
				throw e;
			}
		}

		for (String variableName : bindings.keySet()) {
			try {
				bindings.put(variableName, engine.eval(variableName));
			} catch (Exception e) {
				if(e.getMessage().contains("unresolved reference")) {
					bindings.remove(variableName);
				} else {
					e.printStackTrace();
				}
			}
		}

		engine.put(ScriptBindings.SCRIPT_ID_VAR, parentScriptId);
		embeddedScriptInvoker.setParentScriptId(parentScriptId);*/
	}

	private void putResult(ScriptExecutionResult executionResult, String variableName) {
		try {
			executionResult.put(variableName, engine.eval(variableName));
		} catch (Exception e) {
			if(e.getMessage().contains("unresolved reference")) {
				executionResult.remove(variableName);
			} else {
				e.printStackTrace();
			}
		}
	}

	@Override
	public void release() {
		executorPool.release(this);
	}

	private String getExpectedType(String variableName, ScriptBindings bindings) {
		Object result = bindings.get(variableName);
		if (result instanceof Boolean) {
			return "Boolean";
		} else if (result instanceof Character) {
			return "Char";
		} else if (result instanceof Double) {
			return "Double";
		} else if (result instanceof Float) {
			return "Float";
		} else if (result instanceof Integer) {
			return "Int";
		} else if (result instanceof Long) {
			return "Long";
		} else if (result instanceof Short) {
			return "Short";
		} else if (result instanceof String) {
			return "String";
		} else {
			return result.getClass().getName();
		}
	}
}


<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/util/ScheduledTaskQueueTest.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import org.junit.Assert;
import org.junit.Test;
import org.mini2Dx.miniscript.core.threadpool.ScheduledTask;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class ScheduledTaskQueueTest {
	private static final int MAX_CAPACITY = 5;

	@Test
	public void testDelay() {
		final ScheduledTaskQueue queue = new ScheduledTaskQueue(MAX_CAPACITY);
		final long expectedStartTimeNanos = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L);
		queue.offer(ScheduledTask.allocate(() -> {

		}, expectedStartTimeNanos));

		final AtomicLong result = new AtomicLong(-1L);
		Thread thread = new Thread(() -> {
			try {
				final ScheduledTask task = queue.take();
				result.set(System.nanoTime());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});

		thread.start();
		try {
			thread.join();
		} catch (Exception e) {}

		Assert.assertTrue(result.get() >= expectedStartTimeNanos);
	}

	@Test
	public void testDelayWithEmptyQueue() {
		final ScheduledTaskQueue queue = new ScheduledTaskQueue(MAX_CAPACITY);
		final long expectedStartTimeNanos = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L);

		final AtomicLong result = new AtomicLong(-1L);
		Thread thread = new Thread(() -> {
			try {
				final ScheduledTask task = queue.take();
				result.set(System.nanoTime());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});

		thread.start();
		try {
			Thread.sleep(1000);
		} catch (Exception e) {}

		queue.offer(ScheduledTask.allocate(() -> {

		}, expectedStartTimeNanos));

		try {
			thread.join();
		} catch (Exception e) {}

		Assert.assertTrue(result.get() >= expectedStartTimeNanos);
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/ScriptBindings.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import java.util.*;

/**
 * A mapping of variable names to objects passed to a script
 */
public class ScriptBindings implements Map<String, Object> {
	public static final String SCRIPT_ID_VAR = "scriptId";
	public static final String SCRIPT_PARENT_ID_VAR = "scriptParentId";
	public static final String SCRIPT_INVOKE_VAR = "scripts";

	private final Map<String, Object> bindings = Collections.synchronizedMap(new HashMap<String, Object>());

	/**
	 * Creates a duplicate instance of this {@link ScriptBindings}
	 * 
	 * @return A new {@link ScriptBindings} instance containing all the same
	 *         bindings as this instance
	 */
	public ScriptBindings duplicate() {
		ScriptBindings result = new ScriptBindings();
		result.putAll(bindings);
		return result;
	}

	@Override
	public int size() {
		return bindings.size();
	}

	@Override
	public boolean isEmpty() {
		return bindings.isEmpty();
	}

	@Override
	public boolean containsKey(Object key) {
		return bindings.containsKey(key);
	}

	@Override
	public boolean containsValue(Object value) {
		return bindings.containsValue(value);
	}

	@Override
	public Object get(Object key) {
		return bindings.get(key);
	}

	@Override
	public Object put(String key, Object value) {
		return bindings.put(key, value);
	}

	@Override
	public Object remove(Object key) {
		return bindings.remove(key);
	}

	@Override
	public void putAll(Map<? extends String, ? extends Object> m) {
		bindings.putAll(m);
	}

	@Override
	public void clear() {
		bindings.clear();
	}

	@Override
	public Set<String> keySet() {
		return bindings.keySet();
	}

	@Override
	public Collection<Object> values() {
		return bindings.values();
	}

	@Override
	public Set<java.util.Map.Entry<String, Object>> entrySet() {
		return bindings.entrySet();
	}

	@Override
	public String toString() {
		StringBuilder result = new StringBuilder("ScriptBindings [");
		for (String key : bindings.keySet()) {
			result.append(key);
			result.append("=");
			result.append(bindings.get(key));
			result.append(", ");
		}
		result.delete(result.length() - 2, result.length());
		result.append("]");
		return result.toString();
	}
}


<|start_filename|>kotlin/src/test/resources/nestedInvokeWithinScript.kts<|end_filename|>
val scripts = bindings["scripts"] as org.mini2Dx.miniscript.core.EmbeddedScriptInvoker
scripts.invokeSync("invokeWithinScript.kts")
val stringValue: String = bindings["stringValue"] as String + "4"

<|start_filename|>gradle-plugin/src/main/java/org/mini2Dx/miniscript/gradle/MiniscriptPlugin.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.gradle;

import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;

import java.io.File;

public class MiniscriptPlugin implements Plugin<Project> {
	@Override
	public void apply(final Project project) {
		project.getPlugins().apply(JavaPlugin.class);
		final JavaPluginConvention javaConvention =  project.getConvention().getPlugin(JavaPluginConvention.class);
		final SourceSet mainSourceSet = javaConvention.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME);

		final MiniscriptExtension extension = project.getExtensions().create("miniscript", MiniscriptExtension.class, project);

		project.getTasks().create("generateScriptStubs",
				MiniscriptStubTask.class,
				new Action<MiniscriptStubTask>() {

					@Override
					public void execute(MiniscriptStubTask miniscriptStubTask) {
						miniscriptStubTask.getPrefixWithRoot().set(extension.getPrefixWithRoot());
						miniscriptStubTask.getRecursive().set(extension.getRecursive());
						miniscriptStubTask.getScriptsDir().set(extension.getScriptsDir());
						miniscriptStubTask.getOutputClass().set(extension.getOutputClass());

						for(File srcDir : mainSourceSet.getJava().getSrcDirs()) {
							miniscriptStubTask.setOutputDir(srcDir);
						}
					}
				});
		project.getTasks().create("compileScripts",
				MiniscriptCompileTask.class,
				new Action<MiniscriptCompileTask>() {

			@Override
			public void execute(MiniscriptCompileTask miniscriptCompileTask) {
				miniscriptCompileTask.getPrefixWithRoot().set(extension.getPrefixWithRoot());
				miniscriptCompileTask.getRecursive().set(extension.getRecursive());
				miniscriptCompileTask.getScriptsDir().set(extension.getScriptsDir());
				miniscriptCompileTask.getOutputClass().set(extension.getOutputClass());

				miniscriptCompileTask.setOutputDir(mainSourceSet.getJava().getOutputDir());
			}
		});
	}
}


<|start_filename|>lua/src/main/java/org/mini2Dx/miniscript/lua/LuaScriptExecutor.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.lua;

import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.lib.jse.CoerceJavaToLua;
import org.mini2Dx.miniscript.core.*;
import org.mini2Dx.miniscript.core.exception.ScriptSkippedException;

/**
 * An implementation of {@link ScriptExecutor} for Lua scripts
 */
public class LuaScriptExecutor implements ScriptExecutor<LuaValue> {
	private final LuaScriptExecutorPool executorPool;

	public LuaScriptExecutor(LuaScriptExecutorPool executorPool) {
		this.executorPool = executorPool;
	}

	@Override
	public GameScript<LuaValue> compile(String script) {
		return new PerThreadGameScript<LuaValue>(script);
	}

	@Override
	public ScriptExecutionResult execute(int scriptId, GameScript<LuaValue> script, ScriptBindings bindings, boolean returnResult)
			throws Exception {
		final Globals globals = executorPool.getLocalGlobals();

		final LuaEmbeddedScriptInvoker embeddedScriptInvoker = executorPool.getEmbeddedScriptInvokerPool().allocate();
		embeddedScriptInvoker.setScriptBindings(bindings);
		embeddedScriptInvoker.setScriptExecutor(this);
		embeddedScriptInvoker.setParentScriptId(scriptId);
		
		for (String variableName : bindings.keySet()) {
			globals.set(variableName, CoerceJavaToLua.coerce(bindings.get(variableName)));
		}
		globals.set(ScriptBindings.SCRIPT_PARENT_ID_VAR, CoerceJavaToLua.coerce(-1));
		globals.set(ScriptBindings.SCRIPT_ID_VAR, CoerceJavaToLua.coerce(scriptId));
		globals.set(ScriptBindings.SCRIPT_INVOKE_VAR, CoerceJavaToLua.coerce(embeddedScriptInvoker));

		if (!script.hasScript()) {
			script.setScript(executorPool.compileWithGlobals(globals, script));
		}
		
		try {
			script.getScript().invoke();
		} catch (Exception e) {
			if(e instanceof ScriptSkippedException || e.getCause() instanceof ScriptSkippedException) {
				throw new ScriptSkippedException();
			} else {
				throw e;
			}
		}

		executorPool.getEmbeddedScriptInvokerPool().release(embeddedScriptInvoker);

		if (!returnResult) {
			for (String variableName : bindings.keySet()) {
				globals.set(variableName, CoerceJavaToLua.coerce(null));
			}
			return null;
		}		
		ScriptExecutionResult executionResult = new ScriptExecutionResult(null);
		
		LuaValue [] keys = globals.keys();
		for(int i = 0; i < keys.length; i++) {
			LuaValue key = keys[i];
			LuaValue value = globals.get(key);
			switch ( value.type() ) {
			case LuaValue.TNIL:
				continue;
			case LuaValue.TSTRING:
				executionResult.put(key.tojstring(), value.tojstring());
				continue;
			case LuaValue.TNUMBER:
				if(value.isinttype()) {
					executionResult.put(key.tojstring(), value.toint());
				} else {
					executionResult.put(key.tojstring(), value.todouble());
				}
				continue;
			case LuaValue.TBOOLEAN:
				executionResult.put(key.tojstring(), value.toboolean());
				continue;
			case LuaValue.TUSERDATA:
				if(bindings.containsKey(key.tojstring())) {
					executionResult.put(key.tojstring(), value.checkuserdata(Object.class));
				}
				continue;
			}
		}
		
		for (String variableName : bindings.keySet()) {
			globals.set(variableName, CoerceJavaToLua.coerce(null));
		}
		return executionResult;
	}

	@Override
	public void executeEmbedded(int parentScriptId, int scriptId, GameScript<LuaValue> script,
								EmbeddedScriptInvoker embeddedScriptInvoker, ScriptBindings bindings) throws Exception {
		final Globals globals = executorPool.getLocalGlobals();
		globals.set(ScriptBindings.SCRIPT_ID_VAR, CoerceJavaToLua.coerce(scriptId));
		globals.set(ScriptBindings.SCRIPT_PARENT_ID_VAR, CoerceJavaToLua.coerce(parentScriptId));
		embeddedScriptInvoker.setParentScriptId(scriptId);

		if (!script.hasScript()) {
			script.setScript(executorPool.compileWithGlobals(globals, script));
		}

		try {
			script.getScript().invoke();
		} catch (Exception e) {
			if(e instanceof ScriptSkippedException || e.getCause() instanceof ScriptSkippedException) {
				throw new ScriptSkippedException();
			} else {
				throw e;
			}
		}

		globals.set(ScriptBindings.SCRIPT_ID_VAR, CoerceJavaToLua.coerce(parentScriptId));
		embeddedScriptInvoker.setParentScriptId(parentScriptId);

		LuaValue [] keys = globals.keys();
		for(int i = 0; i < keys.length; i++) {
			LuaValue key = keys[i];
			LuaValue value = globals.get(key);
			switch ( value.type() ) {
			case LuaValue.TNIL:
				continue;
			case LuaValue.TSTRING:
				bindings.put(key.tojstring(), value.tojstring());
				continue;
			case LuaValue.TNUMBER:
				if(value.isinttype()) {
					bindings.put(key.tojstring(), value.toint());
				} else {
					bindings.put(key.tojstring(), value.todouble());
				}
				continue;
			case LuaValue.TBOOLEAN:
				bindings.put(key.tojstring(), value.toboolean());
				continue;
			case LuaValue.TUSERDATA:
				if(bindings.containsKey(key.tojstring())) {
					bindings.put(key.tojstring(), value.checkuserdata(Object.class));
				}
				continue;
			}
		}
	}

	@Override
	public void release() {
		executorPool.release(this);
	}

}


<|start_filename|>kotlin/src/test/resources/default.kts<|end_filename|>
val beginNotificationResult: Boolean = false
val stringValue: String = bindings["stringValue"] as String + "123"
val intValue: Int = 101
val booleanValue: Boolean = true
val scriptId: Int = (bindings["scriptId"] as java.lang.Integer).toInt()


<|start_filename|>kotlin/src/test/resources/waitForCompletion.kts<|end_filename|>
val stringValue: String = bindings["stringValue"] as String + "123"
val intValue: Int = 101
val booleanValue: Boolean = !(bindings["booleanValue"] as Boolean)
val future = bindings["future"] as org.mini2Dx.miniscript.core.GameFuture
future.waitForCompletion()

<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/dummy/DummyGameFuture.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core.dummy;

import org.mini2Dx.miniscript.core.GameFuture;
import org.mini2Dx.miniscript.core.GameScriptingEngine;
import org.mini2Dx.miniscript.core.exception.ScriptSkippedException;

/**
 * A {@link GameFuture} implementation for unit tests
 */
public class DummyGameFuture extends GameFuture {
	private boolean updated = false;
	private boolean futureSkipped = false;
	private boolean scriptSkipped = false;
	private boolean futureCompleted = false;
	private boolean waitOccurred = false;
	private int updateCount = 0;
	
	public DummyGameFuture(GameScriptingEngine gameScriptingEngine) {
		super(gameScriptingEngine);
	}

	@Override
	protected boolean update(float delta) {
		updated = true;
		updateCount++;
		return futureCompleted;
	}

	@Override
	protected void onFutureSkipped() {
		futureSkipped = true;
	}

	@Override
	protected void onScriptSkipped() {
		scriptSkipped = true;
	}
	
	@Override
	public void waitForCompletion() throws ScriptSkippedException {
		waitOccurred = true;
		super.waitForCompletion();
	}

	public boolean isFutureCompleted() {
		return futureCompleted;
	}

	public void setFutureCompleted(boolean futureCompleted) {
		this.futureCompleted = futureCompleted;
	}

	public boolean isUpdated() {
		return updated;
	}

	public boolean isFutureSkippedCalled() {
		return futureSkipped;
	}

	public boolean isScriptSkippedCalled() {
		return scriptSkipped;
	}
	
	public boolean waitOccurred() {
		return waitOccurred;
	}

	public int getUpdateCount() {
		return updateCount;
	}
}


<|start_filename|>groovy/src/test/resources/invokeWithinScript.groovy<|end_filename|>
scripts.invokeSync("default.groovy")
intValue2 = 102

<|start_filename|>lua/src/test/resources/legalSandbox.lua<|end_filename|>
stringValue = stringValue .. "123"
intValue = 101
booleanValue = true
jObject:setValue(78)

<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/SandboxedGameScriptingEngineTest.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mini2Dx.miniscript.core.dummy.DummyGameFuture;
import org.mini2Dx.miniscript.core.dummy.DummyJavaObject;
import org.mini2Dx.miniscript.core.dummy.ScriptResult;
import org.mini2Dx.miniscript.core.exception.InsufficientCompilersException;

/**
 * Base UAT class for {@link GameScriptingEngine} implementations that support sandboxing
 */
public abstract class SandboxedGameScriptingEngineTest {
	protected ScriptBindings scriptBindings;
	protected DummyGameFuture gameFuture;
	protected DummyJavaObject javaObject;
	
	protected GameScriptingEngine scriptingEngine;
	protected AtomicBoolean scriptExecuted;
	protected AtomicReference<ScriptResult> scriptResult;
	
	@Before
	public void setUp() {
		scriptingEngine = createScriptingEngineWithSandboxing();
		
		javaObject = new DummyJavaObject();
		gameFuture = new DummyGameFuture(scriptingEngine);
		scriptExecuted = new AtomicBoolean(false);
		scriptResult = new AtomicReference<ScriptResult>(ScriptResult.NOT_EXECUTED);
		
		scriptBindings = new ScriptBindings();
		scriptBindings.put("stringValue", "hello");
		scriptBindings.put("booleanValue", false);
		scriptBindings.put("intValue", 7);
		scriptBindings.put("future", gameFuture);
		scriptBindings.put("jObject", javaObject);
	}
	
	@After
	public void teardown() {
		scriptingEngine.dispose();
	}
	
	@Test
	public void testSandboxScript() throws InsufficientCompilersException, IOException {
		final int expectedScriptId = scriptingEngine.compileScript(getSandboxScriptFilepath(), getSandboxScript());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {
			
			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}
			
			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		while(!scriptExecuted.get()) {
			scriptingEngine.update(1f);
		}
		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(false, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
		Assert.assertEquals(78, javaObject.getValue());
	}
	
	@Test
	public void testSandboxScriptWithIllegalLines() throws InsufficientCompilersException, IOException {
		final int expectedScriptId = scriptingEngine.compileScript(getSandboxScriptWithIllegalLinesFilepath(), getSandboxScriptWithIllegalLines());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {
			
			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}
			
			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		while(!scriptExecuted.get()) {
			scriptingEngine.update(1f);
		}
		Assert.assertEquals(ScriptResult.EXCEPTION, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(false, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
		Assert.assertEquals(0, javaObject.getValue());
	}
	
	protected abstract GameScriptingEngine createScriptingEngineWithSandboxing();

	protected abstract String getSandboxScriptWithIllegalLinesFilepath();

	protected abstract InputStream getSandboxScriptWithIllegalLines();

	protected abstract String getSandboxScriptFilepath();

	protected abstract InputStream getSandboxScript();
	
	protected boolean checkExpectedScriptResults(ScriptExecutionResult executionResult) {
		if(!executionResult.containsKey("stringValue")) {
			System.err.println("stringValue not present");
			return false;
		}
		if(!executionResult.containsKey("booleanValue")) {
			System.err.println("booleanValue not present");
			return false;
		}
		if(!executionResult.containsKey("intValue")) {
			System.err.println("intValue not present");
			return false;
		}
		if(!"hello123".equals(executionResult.get("stringValue"))) {
			System.err.println("Expected stringValue to be hello123 but was " + executionResult.get("stringValue"));
			return false;
		}
		if(!((Boolean) executionResult.get("booleanValue"))) {
			System.err.println("Expected booleanValue to be true but was " + executionResult.get("stringValue"));
			return false;
		}
		if(((Integer) executionResult.get("intValue")) != 101) {
			System.err.println("Expected intValue to be 101 but was " + executionResult.get("stringValue"));
			return false;
		}
		return true;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/util/ReadWriteBlockingQueue.java<|end_filename|>
/**
 * Copyright 2020 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import java.util.ArrayDeque;

public class ReadWriteBlockingQueue<E> extends AbstractConcurrentBlockingQueue<E> {

	public ReadWriteBlockingQueue(int maxCapacity) {
		super(maxCapacity, new ArrayDeque<>());
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/GameScriptingEngine.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import org.mini2Dx.lockprovider.Locks;
import org.mini2Dx.lockprovider.jvm.JvmLocks;
import org.mini2Dx.miniscript.core.exception.InsufficientCompilersException;
import org.mini2Dx.miniscript.core.exception.NoSuchScriptException;
import org.mini2Dx.miniscript.core.notification.ScriptCancelledNotification;
import org.mini2Dx.miniscript.core.notification.ScriptNotification;
import org.mini2Dx.miniscript.core.notification.ScriptSkippedNotification;
import org.mini2Dx.miniscript.core.threadpool.DefaultThreadPoolProvider;
import org.mini2Dx.miniscript.core.util.*;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Provides scripting functionality to your game
 * 
 * Note that this is a base class for each scripting language implementation.
 */
public abstract class GameScriptingEngine implements Runnable {
	/**
	 * Returns the most recently created {@link GameScriptingEngine}
	 */
	public static GameScriptingEngine MOST_RECENT_INSTANCE = null;

	public static Locks LOCK_PROVIDER = new JvmLocks();

	private static final int DEFAULT_MAX_CONCURRENT_SCRIPTS = 2;

	private final ScriptInvocationPool scriptInvocationPool = new ScriptInvocationPool();
	private final ScriptInvocationQueue scriptInvocationQueue = new ScriptInvocationQueue();
	final Queue<ScriptNotification> scriptNotifications = new ReadWriteArrayQueue<ScriptNotification>();
	private final InteractiveScriptListener interactiveScriptListener;

	final ReadWriteArrayQueue<GameFuture> queuedFutures = new ReadWriteArrayQueue<>();
	final ReadWriteIntMap<GameFuture> runningFutures = new ReadWriteIntMap<GameFuture>();
	private final ReadWriteIntMap<ScriptExecutionTask<?>> runningScripts = new ReadWriteIntMap<ScriptExecutionTask<?>>();
	private final IntSet completedFutures = new IntSet();
	private final IntSet completedScripts = new IntSet();

	private final List<String> tmpRunningScripts = new ArrayList<>();

	private final ThreadPoolProvider threadPoolProvider;
	private final ScriptExecutorPool<?> scriptExecutorPool;

	private ScheduledFuture cleanupTask;
	private boolean cancelReallocatedFutures = true;

	private Thread gameThread = null;

	private final AtomicBoolean shuttingDown = new AtomicBoolean(false);

	/**
	 * Constructs a scripting engine backed by a thread pool with the maximum
	 * amount of concurrent scripts set to 2.
	 * Sandboxing is enabled if the implementation supports it.
	 */
	public GameScriptingEngine() {
		this(DEFAULT_MAX_CONCURRENT_SCRIPTS);
	}

	public GameScriptingEngine(ThreadPoolProvider threadPoolProvider) {
		this(DEFAULT_MAX_CONCURRENT_SCRIPTS, threadPoolProvider);
	}

	/**
	 * Constructs a scripting engine backed by a thread pool. Sandboxing is
	 * enabled if the implementation supports it.
	 * 
	 * @param maxConcurrentScripts
	 *            The maximum amount of concurrently running scripts. WARNING:
	 *            this is a 'requested' amount and may be less due to the amount
	 *            of available processors on the player's machine.
	 */
	public GameScriptingEngine(int maxConcurrentScripts) {
		this(new NoopClasspathScriptProvider(), maxConcurrentScripts);
	}

	public GameScriptingEngine(int maxConcurrentScripts, ThreadPoolProvider threadPoolProvider) {
		this(new NoopClasspathScriptProvider(), maxConcurrentScripts, threadPoolProvider);
	}

	/**
	 * Constructs a scripting engine backed by a thread pool. Sandboxing is
	 * enabled if the implementation supports it.
	 * @param classpathScriptProvider The auto-generated {@link ClasspathScriptProvider} for the game
	 * @param maxConcurrentScripts The maximum amount of concurrently running scripts. WARNING:
	 *                                this is a 'requested' amount and may be less due to the amount
	 *                                of available processors on the player's machine.
	 */
	public GameScriptingEngine(ClasspathScriptProvider classpathScriptProvider, int maxConcurrentScripts) {
		super();
		interactiveScriptListener = new InteractiveScriptListener(this, scriptInvocationQueue);
		scriptExecutorPool = createScriptExecutorPool(classpathScriptProvider, maxConcurrentScripts, isSandboxingSupported());

		threadPoolProvider = new DefaultThreadPoolProvider(maxConcurrentScripts + 1);
		init(maxConcurrentScripts);
	}

	public GameScriptingEngine(ClasspathScriptProvider classpathScriptProvider, int maxConcurrentScripts, ThreadPoolProvider threadPoolProvider) {
		super();
		interactiveScriptListener = new InteractiveScriptListener(this, scriptInvocationQueue);
		scriptExecutorPool = createScriptExecutorPool(classpathScriptProvider, maxConcurrentScripts, isSandboxingSupported());

		this.threadPoolProvider = threadPoolProvider;
		init(maxConcurrentScripts);
	}

	/**
	 * Constructs a scripting engine backed by a thread pool with the maximum
	 * amount of concurrent scripts set to the amount of processors + 1.
	 * 
	 * @param sandboxed
	 *            True if script sandboxing should be enabled
	 */
	public GameScriptingEngine(boolean sandboxed) {
		this(new NoopClasspathScriptProvider(), sandboxed);
	}

	public GameScriptingEngine(ThreadPoolProvider threadPoolProvider, boolean sandboxed) {
		this(new NoopClasspathScriptProvider(), threadPoolProvider, sandboxed);
	}

	/**
	 * Constructs a scripting engine backed by a thread pool with the maximum
	 * amount of concurrent scripts set to 2.
	 * @param classpathScriptProvider The auto-generated {@link ClasspathScriptProvider} for the game
	 * @param sandboxed True if script sandboxing should be enabled
	 */
	public GameScriptingEngine(ClasspathScriptProvider classpathScriptProvider, boolean sandboxed) {
		this(classpathScriptProvider,DEFAULT_MAX_CONCURRENT_SCRIPTS, sandboxed);
	}

	public GameScriptingEngine(ClasspathScriptProvider classpathScriptProvider, ThreadPoolProvider threadPoolProvider, boolean sandboxed) {
		this(classpathScriptProvider,DEFAULT_MAX_CONCURRENT_SCRIPTS, threadPoolProvider, sandboxed);
	}

	/**
	 * Constructs a scripting engine backed by a thread pool.
	 * 
	 * @param maxConcurrentScripts
	 *            The maximum amount of concurrently running scripts. WARNING:
	 *            this is a 'requested' amount and may be less due to the amount
	 *            of available processors on the player's machine.
	 * @param sandboxed
	 *            True if script sandboxing should be enabled
	 */
	public GameScriptingEngine(int maxConcurrentScripts, boolean sandboxed) {
		this(new NoopClasspathScriptProvider(), maxConcurrentScripts, sandboxed);
	}

	public GameScriptingEngine(int maxConcurrentScripts, ThreadPoolProvider threadPoolProvider, boolean sandboxed) {
		this(new NoopClasspathScriptProvider(), maxConcurrentScripts, threadPoolProvider, sandboxed);
	}

	/**
	 * Constructs a scripting engine backed by a thread pool.
	 * @param classpathScriptProvider The auto-generated {@link ClasspathScriptProvider} for the game
	 * @param maxConcurrentScripts The maximum amount of concurrently running scripts. WARNING:
	 *                                this is a 'requested' amount and may be less due to the amount
	 *                                of available processors on the player's machine.
	 * @param sandboxed True if script sandboxing should be enabled
	 */
	public GameScriptingEngine(ClasspathScriptProvider classpathScriptProvider,
	                           int maxConcurrentScripts, boolean sandboxed) {
		super();
		interactiveScriptListener = new InteractiveScriptListener(this, scriptInvocationQueue);
		scriptExecutorPool = createScriptExecutorPool(classpathScriptProvider, maxConcurrentScripts, sandboxed);

		threadPoolProvider = new DefaultThreadPoolProvider(maxConcurrentScripts + 1);
		init(maxConcurrentScripts);
	}

	public GameScriptingEngine(ClasspathScriptProvider classpathScriptProvider,
	                           int maxConcurrentScripts, ThreadPoolProvider threadPoolProvider, boolean sandboxed) {
		super();
		interactiveScriptListener = new InteractiveScriptListener(this, scriptInvocationQueue);
		scriptExecutorPool = createScriptExecutorPool(classpathScriptProvider, maxConcurrentScripts, sandboxed);

		this.threadPoolProvider = threadPoolProvider;
		init(maxConcurrentScripts);
	}

	private void init(int maxConcurrentScripts) {
		for(int i = 0; i < maxConcurrentScripts; i++) {
			threadPoolProvider.scheduleAtFixedRate(this, 16L, 16L, TimeUnit.MILLISECONDS);
		}
		threadPoolProvider.scheduleAtFixedRate(new Runnable() {
			@Override
			public void run() {
				try {
					cleanupCompletedFutures();
					cleanupCompletedScripts();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}, 0L, 1L, TimeUnit.SECONDS);
		MOST_RECENT_INSTANCE = this;
	}

	/**
	 * Shuts down the thread pool and cleans up resources
	 */
	public void dispose() {
		dispose(false);
	}

	/**
	 * Shuts down the thread pool and cleans up resources
	 * @param interruptScripts True if running scripts should be interrupted
	 */
	public void dispose(boolean interruptScripts) {
		shuttingDown.set(true);

		if(cleanupTask != null) {
			cleanupTask.cancel(false);
			cleanupTask = null;
		}
		threadPoolProvider.shutdown(interruptScripts);

		if(!interruptScripts) {
			return;
		}
		cancelAllQueuedScripts();
		skipAllQueuedGameFutures();
		skipAllScripts();
	}

	/**
	 * Checks if sandboxing is supported by the {@link GameScriptingEngine}
	 * implementation
	 * 
	 * @return True if sandboxing is supported
	 */
	public abstract boolean isSandboxingSupported();

	/**
	 * Checks if scripts can be synchronously run within other scripts
	 *
	 * @return True if scripts can be invoke synchronously within scripts
	 */
	public abstract boolean isEmbeddedSynchronousScriptSupported();

	protected abstract ScriptExecutorPool<?> createScriptExecutorPool(ClasspathScriptProvider classpathScriptProvider,
	                                                                  int poolSize, boolean sandboxing);

	/**
	 * Updates all {@link GameFuture}s
	 * 
	 * @param delta
	 *            The time (in seconds) since the last frame update
	 */
	public void update(float delta) {
		if(gameThread == null) {
			gameThread = Thread.currentThread();
		}

		for (GameFuture gameFuture : runningFutures.values()) {
			if(gameFuture == null) {
				continue;
			}
			gameFuture.evaluate(delta);
		}

		while (!queuedFutures.isEmpty()) {
			GameFuture nextFuture = queuedFutures.poll();
			GameFuture previousFuture = runningFutures.put(nextFuture.getFutureId(), nextFuture);
			if (previousFuture == null) {
				continue;
			}
			if (!cancelReallocatedFutures) {
				continue;
			}
			try {
				previousFuture.skipFuture();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		while (!scriptNotifications.isEmpty()) {
			scriptNotifications.poll().process();
		}
	}

	/**
	 * This should not be invoked by the developer. Call {@link #update(float)}
	 * instead.
	 */
	@Override
	public void run() {
		if(shuttingDown.get()) {
			return;
		}
		long startTime = System.currentTimeMillis();
		ScriptInvocation scriptInvocation = null;
		try {
			while ((scriptInvocation = scriptInvocationQueue.poll()) != null) {
				if(shuttingDown.get()) {
					continue;
				}
				final ScriptInvocationListener invocationListener;
				if(scriptInvocation.isInteractive()) {
					interactiveScriptListener.track(scriptInvocation.getScriptId(), scriptInvocation.getInvocationListener());
					invocationListener = interactiveScriptListener;
				} else {
					invocationListener = scriptInvocation.getInvocationListener();
				}

				ScriptExecutionTask<?> executionTask = scriptExecutorPool.execute(scriptInvocation.getTaskId(),
						scriptInvocation.getScriptId(), scriptInvocation.getScriptBindings(), invocationListener, false);
				Future<?> taskFuture = threadPoolProvider.submit(executionTask);
				executionTask.setTaskFuture(taskFuture);
				runningScripts.put(executionTask.getTaskId(), executionTask);
				scriptInvocation.release();
			}
		} catch (NoSuchScriptException e) {
			if(scriptInvocation != null) {
				if(scriptInvocation.getInvocationListener() != null) {
					scriptInvocation.getInvocationListener().onScriptException(scriptInvocation.getScriptId(), e);
				}
				interactiveScriptListener.onScriptException(scriptInvocation.getScriptId(), e);
			} else {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		if(shuttingDown.get()) {
			return;
		}
	}

	private void cleanupCompletedFutures() {
		for (GameFuture gameFuture : runningFutures.values()) {
			if(gameFuture == null) {
				continue;
			}
			if (gameFuture.isReadyForGC()) {
				completedFutures.add(gameFuture.getFutureId());
			}
		}
		final IntSet.IntSetIterator iterator = completedFutures.iterator();
		while(iterator.hasNext) {
			final int futureId = iterator.next();
			runningFutures.remove(futureId);
		}
		completedFutures.clear();
	}

	private void cleanupCompletedScripts() {
		for (ScriptExecutionTask<?> scriptExecutionTask : runningScripts.values()) {
			if(scriptExecutionTask == null) {
				continue;
			}
			if (scriptExecutionTask.isFinished()) {
				scriptExecutionTask.cleanup();
				completedScripts.add(scriptExecutionTask.getTaskId());
			}
		}
		final IntSet.IntSetIterator iterator = completedScripts.iterator();
		while(iterator.hasNext) {
			final int taskId = iterator.next();
			runningScripts.remove(taskId);
		}
		completedScripts.clear();
	}

	/**
	 * Skips all currently running scripts
	 */
	public void skipAllScripts() {
		for (ScriptExecutionTask<?> scriptExecutionTask : runningScripts.values()) {
			if(scriptExecutionTask == null) {
				continue;
			}
			scriptExecutionTask.skipScript();
		}
	}

	/**
	 * Skips all running instances of a script
	 * 
	 * @param scriptId
	 *            The ID of the script to skip
	 */
	public void skipScript(int scriptId) {
		final IntMap.Keys keys = runningScripts.keys();
		while(keys.hasNext) {
			final int taskId = keys.next();
			ScriptExecutionTask<?> scriptExecutionTask = runningScripts.get(taskId);
			if (scriptExecutionTask == null) {
				continue;
			}
			if (scriptExecutionTask.getScriptId() != scriptId) {
				continue;
			}
			scriptExecutionTask.skipScript();
		}
	}

	/**
	 * Skips a specific running instance of a script
	 *
	 * @param taskId The ID of the task to skip
	 */
	public void skipScriptByTaskId(int taskId) {
		final IntMap.Keys keys = runningScripts.keys();
		while(keys.hasNext) {
			final int otherTaskId = keys.next();
			if(taskId != otherTaskId) {
				continue;
			}
			ScriptExecutionTask<?> scriptExecutionTask = runningScripts.get(taskId);
			if (scriptExecutionTask == null) {
				continue;
			}
			scriptExecutionTask.skipScript();
		}
	}

	/**
	 * Skips all currently running {@link GameFuture}s
	 */
	public void skipAllRunningGameFutures() {
		for (GameFuture gameFuture : runningFutures.values()) {
			if(gameFuture == null) {
				continue;
			}
			gameFuture.skipFuture();
		}
	}

	/**
	 * Skips all currently queued {@link GameFuture}s
	 */
	public void skipAllQueuedGameFutures() {
		while(!queuedFutures.isEmpty()) {
			final GameFuture gameFuture = queuedFutures.poll();
			if(gameFuture == null) {
				continue;
			}
			gameFuture.skipFuture();
		}
	}

	/**
	 * Removes all currently running {@link GameFuture}s without sending skipFuture event
	 */
	public void cancelAllRunningGameFutures() {
		runningFutures.clear();
	}

	/**
	 * Removes all currently queued {@link GameFuture}s without sending skipFuture event
	 */
	public void cancelAllQueuedGameFutures() {
		queuedFutures.clear();
	}

	/**
	 * Removes all currently queued {@link GameFuture}s without sending skipFuture event
	 */
	public void cancelAllQueuedScripts() {
		cancelAllQueuedScripts(true);
	}

	/**
	 * Removes all currently queued {@link GameFuture}s without sending skipFuture event
	 * @param notifyListeners If true will notify invocation listeners of script cancellation
	 */
	public void cancelAllQueuedScripts(boolean notifyListeners) {
		if(!notifyListeners) {
			scriptInvocationQueue.clear();
			return;
		}

		final List<ScriptInvocation> scriptInvocations = new ArrayList<>();
		scriptInvocationQueue.clear(scriptInvocations);
		notifyScriptCancelled(scriptInvocations);
	}

	/**
	 * Removes all currently queued scripts with a given script ID
	 */
	public void cancelQueuedScript(int scriptId) {
		cancelQueuedScript(scriptId, true);
	}

	/**
	 * Removes a specific queued script by its task ID
	 */
	public void cancelQueuedScriptByTaskId(int taskId) {
		cancelQueuedScriptByTaskId(taskId, true);
	}

	/**
	 * Removes all currently queued scripts with a given script ID
	 * @param notifyListeners If true will notify invocation listeners of script cancellation
	 */
	public void cancelQueuedScript(int scriptId, boolean notifyListeners) {
		final List<ScriptInvocation> scriptInvocations = new ArrayList<>();
		scriptInvocationQueue.cancelByScriptId(scriptId, scriptInvocations);
		if(!notifyListeners) {
			scriptInvocations.clear();
			return;
		}

		notifyScriptCancelled(scriptInvocations);
	}

	/**
	 * Removes a specific queued script by its task ID
	 * @param notifyListeners If true will notify invocation listeners of script cancellation
	 */
	public void cancelQueuedScriptByTaskId(int taskId, boolean notifyListeners) {
		final List<ScriptInvocation> scriptInvocations = new ArrayList<>();
		scriptInvocationQueue.cancelByTaskId(taskId, scriptInvocations);
		if(!notifyListeners) {
			scriptInvocations.clear();
			return;
		}

		notifyScriptCancelled(scriptInvocations);
	}

	/**
	 * Clears all interactive scripts queued
	 */
	public void cancelAllQueuedInteractiveScripts() {
		cancelAllQueuedInteractiveScripts(true);
	}

	/**
	 * Clears all non-interactive scripts queued
	 */
	public void cancelAllQueuedNonInteractiveScripts() {
		cancelAllQueuedNonInteractiveScripts(true);
	}

	/**
	 * Clears all interactive scripts queued
	 * @param notifyListeners If true will send the scriptSkipped event any listener associated with a cancelled invoke
	 */
	public void cancelAllQueuedInteractiveScripts(boolean notifyListeners) {
		if(!notifyListeners) {
			scriptInvocationQueue.clearInteractiveScriptQueue();
			return;
		}

		final List<ScriptInvocation> scriptInvocations = new ArrayList<>();
		scriptInvocationQueue.clearInteractiveScriptQueue(scriptInvocations);
		notifyScriptCancelled(scriptInvocations);
	}

	/**
	 * Clears all non-interactive scripts queued
	 * @param notifyListeners If true will send the scriptSkipped event any listener associated with a cancelled invoke
	 */
	public void cancelAllQueuedNonInteractiveScripts(boolean notifyListeners) {
		if(!notifyListeners) {
			scriptInvocationQueue.clearNonInteractiveScriptQueue();
			return;
		}

		final List<ScriptInvocation> scriptInvocations = new ArrayList<>();
		scriptInvocationQueue.clearNonInteractiveScriptQueue(scriptInvocations);
		notifyScriptCancelled(scriptInvocations);
	}

	/**
	 * Cancels a queued script or skips it if it is currently running
	 * @param scriptId The script ID
	 */
	public void skipOrCancelScript(int scriptId) {
		try {
			skipScript(scriptId);
		} catch (Exception e) {
			e.printStackTrace();
		}
		try {
			cancelQueuedScript(scriptId);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Cancels a specific invocation of a script by its task ID, or skips it if it is currently running
	 * @param taskId The task ID
	 */
	public void skipOrCancelScriptByTaskId(int taskId) {
		try {
			skipScriptByTaskId(taskId);
		} catch (Exception e) {
			e.printStackTrace();
		}
		try {
			cancelQueuedScriptByTaskId(taskId);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Compiles a script for execution. Note it is best to call this
	 * sequentially before any script executions to avoid throwing a
	 * {@link InsufficientCompilersException}.
	 *
	 * Note: If the filepath has already been compiled, the script will not be compiled and the previous compilation is used.
	 *
	 * @param filepath The filepath to store for the script
	 * @param scriptContent
	 *            The text contents of the script
	 * @return The unique id for the script
	 * @throws InsufficientCompilersException
	 *             Thrown if there are no script compilers available
	 * @throws InsufficientCompilersException
	 *             Thrown if there are no script compilers available
	 */
	public int compileScript(String filepath, String scriptContent) throws InsufficientCompilersException {
		final int existingId = scriptExecutorPool.getCompiledScriptId(filepath);
		if(existingId > -1) {
			return existingId;
		}
		return scriptExecutorPool.preCompileScript(filepath, scriptContent);
	}

	/**
	 * Compiles a script for execution. Note it is best to call this
	 * sequentially before any script executions to avoid throwing a
	 * {@link InsufficientCompilersException}.
	 *
	 * @param scriptContent
	 *            The text contents of the script
	 * @return The unique id for the script
	 * @throws InsufficientCompilersException
	 *             Thrown if there are no script compilers available
	 * @throws InsufficientCompilersException
	 *             Thrown if there are no script compilers available
	 */
	public int compileScript(String scriptContent) throws InsufficientCompilersException {
		return compileScript(String.valueOf(scriptContent.hashCode()), scriptContent);
	}

	/**
	 * Returns the script ID for a given filepath
	 * @param filepath The filepath to lookup
	 * @return -1 if the script has not been compiled
	 */
	public int getCompiledScriptId(String filepath) {
		return scriptExecutorPool.getCompiledScriptId(filepath);
	}

	/**
	 * Compiles a script for execution. Note it is best to call this
	 * sequentially before any script executions to avoid throwing a
	 * {@link InsufficientCompilersException}
	 * 
	 * @param inputStream
	 *            The {@link InputStream} to read the script contents from
	 * @return The unique id for the script
	 * @throws InsufficientCompilersException
	 *             Thrown if there are no script compilers available
	 * @throws IOException
	 *             Throw if the {@link InputStream} could not be read or closed
	 */
	public int compileScript(String filepath, InputStream inputStream) throws InsufficientCompilersException, IOException {
		Scanner scanner = new Scanner(inputStream);
		scanner.useDelimiter("\\A");
		String contents = scanner.hasNext() ? scanner.next() : "";
		scanner.close();
		inputStream.close();
		return compileScript(filepath, contents);
	}

	/**
	 * Queues a compiled script for execution in the engine's thread pool
	 * 
	 * @param scriptId
	 *            The id of the script to run
	 * @param scriptBindings
	 *            The variable bindings for the script
	 * @return The unique task ID for this invocation
	 */
	public int invokeCompiledScript(int scriptId, ScriptBindings scriptBindings) {
		return invokeCompiledScript(scriptId, scriptBindings, null);
	}

	/**
	 * Queues a compiled script for execution in the engine's thread pool
	 * 
	 * @param scriptId
	 *            The id of the script to run
	 * @param scriptBindings
	 *            The variable bindings for the script
	 * @param invocationListener
	 *            A {@link ScriptInvocationListener} to list for invocation
	 *            results
	 * @return The unique task ID for this invocation
	 */
	public int invokeCompiledScript(int scriptId, ScriptBindings scriptBindings,
			ScriptInvocationListener invocationListener) {
		return invokeCompiledScript(scriptId, scriptBindings, invocationListener, 0);
	}

	/**
	 * Queues a compiled script for execution in the engine's thread pool
	 *
	 * @param scriptId
	 *            The id of the script to run
	 * @param scriptBindings
	 *            The variable bindings for the script
	 * @param invocationListener
	 *            A {@link ScriptInvocationListener} to list for invocation results
	 * @param priority The script execution priority (higher value = higher priority)
	 * @return The unique task ID for this invocation
	 */
	public int invokeCompiledScript(int scriptId, ScriptBindings scriptBindings,
									 ScriptInvocationListener invocationListener, int priority) {
		return invokeCompiledScript(scriptId, scriptBindings, invocationListener, priority, false);
	}

	/**
	 * Queues a compiled script for execution in the engine's thread pool
	 *
	 * @param scriptId
	 *            The id of the script to run
	 * @param scriptBindings
	 *            The variable bindings for the script
	 * @param invocationListener
	 *            A {@link ScriptInvocationListener} to list for invocation results
	 * @param priority The script execution priority (higher value = higher priority)
	 * @return The unique task ID for this invocation
	 */
	public int invokeCompiledScript(int scriptId, ScriptBindings scriptBindings,
	                                 ScriptInvocationListener invocationListener, int priority, boolean interactive) {
		final ScriptInvocation invocation = scriptInvocationPool.allocate(scriptId, scriptBindings, invocationListener, priority, interactive);
		scriptInvocationQueue.offer(invocation);
		return invocation.getTaskId();
	}

	/**
	 * Compiles and queues a script for execution in the engine's thread pool
	 * 
	 * @param scriptContent
	 *            The text content of the script
	 * @param scriptBindings
	 *            The variable bindings for the script
	 * @return The unique id for the script
	 * @throws InsufficientCompilersException
	 *             Thrown if there are no script compilers available
	 */
	public int invokeScript(String scriptContent, ScriptBindings scriptBindings) throws InsufficientCompilersException {
		return invokeScript(scriptContent, scriptBindings, null);
	}

	/**
	 * Compiles and queues a script for execution in the engine's thread pool
	 * 
	 * @param scriptContent
	 *            The text content of the script
	 * @param scriptBindings
	 *            The variable bindings for the script
	 * @param invocationListener
	 *            A {@link ScriptInvocationListener} to list for invocation
	 *            results
	 * @return The unique id for the script
	 * @throws InsufficientCompilersException
	 *             Thrown if there are no script compilers available
	 */
	public int invokeScript(String scriptContent, ScriptBindings scriptBindings,
			ScriptInvocationListener invocationListener) throws InsufficientCompilersException {
		int scriptId = compileScript(String.valueOf(scriptContent.hashCode()), scriptContent);
		invokeCompiledScript(scriptId, scriptBindings, invocationListener);
		return scriptId;
	}

	/**
	 * Compiles and queues a script for execution in the engine's thread pool
	 *
	 * @param scriptContent
	 *            The text content of the script
	 * @param scriptBindings
	 *            The variable bindings for the script
	 * @param invocationListener
	 *            A {@link ScriptInvocationListener} to list for invocation
	 *            results
	 * @param priority The script execution priority (higher value = higher priority)
	 * @return The unique id for the script
	 * @throws InsufficientCompilersException
	 *             Thrown if there are no script compilers available
	 */
	public int invokeScript(String scriptContent, ScriptBindings scriptBindings,
							ScriptInvocationListener invocationListener, int priority) throws InsufficientCompilersException {
		int scriptId = compileScript(String.valueOf(scriptContent.hashCode()), scriptContent);
		invokeCompiledScript(scriptId, scriptBindings, invocationListener, priority);
		return scriptId;
	}

	/**
	 * Executes a compiled script synchronously on the thread calling this method.
	 * 
	 * Warning: If no {@link ScriptExecutor}s are available this will block
	 * until one is available
	 *
	 * @param taskId The task id
	 * @param scriptId The script id
	 * @param scriptBindings The variable bindings for the script
	 */
	public void invokeCompiledScriptSync(int taskId, int scriptId, ScriptBindings scriptBindings) {
		invokeCompiledScriptSync(taskId, scriptId, scriptBindings, null);
	}

	/**
	 * Executes a compiled script synchronously on the thread calling this method.
	 * 
	 * Warning: If no {@link ScriptExecutor}s are available this will block
	 * until one is available
	 *
	 * @param taskId The task id
	 * @param scriptId The script id
	 * @param scriptBindings The variable bindings for the script
	 * @param invocationListener
	 *            A {@link ScriptInvocationListener} to list for invocation
	 *            results
	 */
	public void invokeCompiledScriptSync(int taskId, int scriptId, ScriptBindings scriptBindings,
										 ScriptInvocationListener invocationListener) {
		ScriptExecutionTask<?> executionTask = scriptExecutorPool.execute(taskId, scriptId, scriptBindings, invocationListener, true);
		runningScripts.put(executionTask.getTaskId(), executionTask);
		executionTask.run();
	}

	/**
	 * Returns the list of currently running scripts.
	 *
	 * Note: This list reference is re-used on every invocation of this method
	 * @return An empty list if nothing running
	 */
	public List<String> getRunningScripts() {
		tmpRunningScripts.clear();
		final IntMap.Keys keys = runningScripts.keys();
		while(keys.hasNext) {
			final int taskId = keys.next();
			ScriptExecutionTask<?> scriptExecutionTask = runningScripts.get(taskId);
			if (scriptExecutionTask == null) {
				continue;
			}
			tmpRunningScripts.add(scriptExecutorPool.getCompiledScriptPath(scriptExecutionTask.getScriptId()));
		}
		return tmpRunningScripts;
	}

	/**
	 * Returns the total scripts (interactive + non-interactive) queued
	 * @return 0 if none
	 */
	public int getTotalScriptsQueued() {
		return scriptInvocationQueue.size();
	}

	/**
	 * Returns the total interactive scripts queued
	 * @return 0 if none
	 */
	public int getTotalInteractiveScriptsQueued() {
		return scriptInvocationQueue.getInteractiveScriptsQueued();
	}

	/**
	 * Returns the total non-interactive scripts queued
	 * @return 0 if none
	 */
	public int getTotalNonInteractiveScriptsQueued() {
		return scriptInvocationQueue.getNonInteractiveScriptsQueued();
	}

	/**
	 * Returns true if interactive script is running
	 * @return
	 */
	public boolean isInteractiveScriptRunning() {
		return scriptInvocationQueue.isInteractiveScriptRunnung();
	}

	void submitGameFuture(GameFuture gameFuture) {
		if(gameThread != null && gameThread == Thread.currentThread()) {
			queuedFutures.offer(gameFuture);
		} else {
			queuedFutures.lazyOffer(gameFuture);
		}
	}

	/**
	 * Sets if newly {@link GameFuture}s should cancel a previously scheduled
	 * {@link GameFuture} with the same ID. IDs attempt be unique but if there
	 * are {@link GameFuture}s that never complete IDs may come into conflict
	 * after millions of generated {@link GameFuture}s. Defaults to true.
	 * 
	 * @param cancelReallocatedFutures
	 *            True if existing {@link GameFuture}s with same ID should be
	 *            cancelled
	 */
	public void setCancelReallocatedFutures(boolean cancelReallocatedFutures) {
		this.cancelReallocatedFutures = cancelReallocatedFutures;
	}


	private void notifyScriptCancelled(List<ScriptInvocation> scriptInvocations) {
		for(ScriptInvocation invocation : scriptInvocations) {
			if(invocation.getInvocationListener() == null) {
				return;
			}
			final ScriptInvocationListener invocationListener = invocation.getInvocationListener();
			if(invocationListener.callOnGameThread()) {
				scriptNotifications
						.offer(new ScriptCancelledNotification(invocationListener, invocation.getScriptId()));
			} else {
				invocationListener.onScriptCancelled(invocation.getScriptId());
			}
		}
		scriptInvocations.clear();
	}
}


<|start_filename|>groovy/src/test/resources/nestedInvokeWithinScript.groovy<|end_filename|>
scripts.invokeSync("invokeWithinScript.groovy")
stringValue += "4";

<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/AbstractGameScriptingEngineTest.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import java.io.InputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mini2Dx.miniscript.core.dummy.DummyGameFuture;
import org.mini2Dx.miniscript.core.dummy.ScriptResult;

/**
 * Base UAT class for {@link GameScriptingEngine} implementations
 */
public abstract class AbstractGameScriptingEngineTest {
	protected ScriptBindings scriptBindings;
	protected DummyGameFuture gameFuture;
	
	protected GameScriptingEngine scriptingEngine;
	protected AtomicBoolean scriptExecuted;
	protected AtomicReference<ScriptResult> scriptResult;
	
	@Before
	public void setUp() {
		scriptingEngine = createScriptingEngine();
		
		gameFuture = new DummyGameFuture(scriptingEngine);
		scriptExecuted = new AtomicBoolean(false);
		scriptResult = new AtomicReference<ScriptResult>(ScriptResult.NOT_EXECUTED);
		
		scriptBindings = new ScriptBindings();
		scriptBindings.put("stringValue", "hello");
		scriptBindings.put("booleanValue", false);
		scriptBindings.put("intValue", 7);
		scriptBindings.put("future", gameFuture);
	}
	
	@After
	public void teardown() {
		scriptingEngine.dispose();
	}
	
	@Test
	public void testInvokeScript() throws Exception {
		final AtomicLong scriptIdVariableResult = new AtomicLong(-1);

		final int expectedScriptId = scriptingEngine.compileScript(getDefaultScript());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {
			
			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}

				scriptIdVariableResult.set(((Number) executionResult.get(ScriptBindings.SCRIPT_ID_VAR)).longValue());
			}
			
			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		while(!scriptExecuted.get()) {
			scriptingEngine.update(1f);
		}
		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(false, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
		Assert.assertEquals(expectedScriptId, scriptIdVariableResult.get());
	}
	
	@Test
	public void testInvokeScriptLocally() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getDefaultScript());
		scriptingEngine.invokeCompiledScriptSync(-1, expectedScriptId, scriptBindings, new ScriptInvocationListener() {
			
			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}
			
			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}
			
			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		scriptingEngine.update(1f);
		scriptingEngine.update(1f);
		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(false, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
	}
	
	@Test
	public void testInvokeScriptViaInputStream() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getDefaultScriptFilepath(), getDefaultScriptInputStream());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {
			
			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}
			
			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}
			
			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		final long timeout = 20000L;
		long timer = 0L;

		while(!scriptExecuted.get() && timer < timeout) {
			long startTime = System.currentTimeMillis();
			scriptingEngine.update(1f);
			timer += System.currentTimeMillis() - startTime;
		}

		if(timer >= timeout) {
			Assert.fail("Timed out after " + timeout + "ms wait for script");
		}

		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(false, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
	}

	@Test
	public void testInvokeScriptBeginNotificationOnGameThread() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getDefaultScript());
		final AtomicBoolean notificationReceived = new AtomicBoolean(false);
		final AtomicLong notificationThreadId = new AtomicLong(-1);
		final AtomicBoolean beginResult = new AtomicBoolean(false);

		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {

			@Override
			public void onScriptBegin(int scriptId) {
				try {
					Thread.sleep(500);
				} catch (Exception e) {}
				scriptBindings.put("beginNotificationResult", true);
				notificationReceived.set(true);
				notificationThreadId.set(Thread.currentThread().getId());
			}

			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);

					//Ensure begin is processed before execution
					beginResult.set(executionResult.containsKey("beginNotificationResult"));
				}
			}

			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		while(!scriptExecuted.get()) {
			if(!notificationReceived.get()) {
				try {
					Thread.sleep(100);
				} catch (Exception e) {}
			}

			scriptingEngine.update(1f);
		}
		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(false, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
		Assert.assertEquals(true, notificationReceived.get());
		Assert.assertEquals(Thread.currentThread().getId(), notificationThreadId.get());
	}

	@Test
	public void testInvokeInteractiveScriptBeginNotificationOnGameThread() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getDefaultScript());
		final AtomicBoolean notificationReceived = new AtomicBoolean(false);
		final AtomicLong notificationThreadId = new AtomicLong(-1);
		final AtomicBoolean beginResult = new AtomicBoolean(false);

		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {

			@Override
			public void onScriptBegin(int scriptId) {
				try {
					Thread.sleep(500);
				} catch (Exception e) {}
				scriptBindings.put("beginNotificationResult", true);
				notificationReceived.set(true);
				notificationThreadId.set(Thread.currentThread().getId());
			}

			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);

					//Ensure begin is processed before execution
					beginResult.set(executionResult.containsKey("beginNotificationResult"));
				}
			}

			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		}, 0, true);
		while(!scriptExecuted.get()) {
			if(!notificationReceived.get()) {
				try {
					Thread.sleep(100);
				} catch (Exception e) {}
			}

			scriptingEngine.update(1f);
		}
		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(false, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
		Assert.assertEquals(true, notificationReceived.get());
		Assert.assertEquals(Thread.currentThread().getId(), notificationThreadId.get());
	}

	@Test
	public void testScriptCancelNotification() throws Exception {
		scriptingEngine.invokeCompiledScript(scriptingEngine.compileScript(getWaitForCompletionScript()), scriptBindings);
		scriptingEngine.invokeCompiledScript(scriptingEngine.compileScript(getWaitForCompletionScript()), scriptBindings);

		final int expectedScriptId = scriptingEngine.compileScript(getDefaultScript());
		final AtomicInteger cancelledScriptId = new AtomicInteger(-1);

		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {

			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}

			@Override
			public void onScriptCancelled(int scriptId) {
				cancelledScriptId.set(scriptId);
				scriptResult.set(ScriptResult.CANCELLED);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		}, 0, false);

		long timer = 0L;
		while(!scriptExecuted.get()) {
			long startTime = System.currentTimeMillis();
			scriptingEngine.update(1f);
			try {
				Thread.sleep(10);
			} catch (Exception e) {}
			timer += System.currentTimeMillis() - startTime;

			if(timer >= 1000L) {
				scriptingEngine.cancelQueuedScript(expectedScriptId);
			}
		}
		Assert.assertEquals(ScriptResult.CANCELLED, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(true, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
		Assert.assertEquals(expectedScriptId, cancelledScriptId.get());
	}

	@Test
	public void testWaitForCompletion() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getWaitForCompletionScript());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {
			
			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}
			
			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}
			
			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		final long timeout = 20000L;
		long timer = 0L;

		while(!scriptExecuted.get() && timer < timeout) {
			long startTime = System.currentTimeMillis();
			gameFuture.setFutureCompleted(true);
			scriptingEngine.update(1f);
			timer += System.currentTimeMillis() - startTime;
		}
		scriptingEngine.update(1f);

		if(timer >= timeout) {
			Assert.fail("Timed out after " + timeout + "ms wait for script");
		}

		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(true, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
		Assert.assertEquals(1, gameFuture.getUpdateCount());
	}
	
	@Test
	public void testSkipFuture() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getWaitForCompletionScript());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {
			
			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				scriptResult.set(ScriptResult.SUCCESS);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}
			
			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		final long timeout = 20000L;
		long timer = 0L;

		while(!scriptExecuted.get() && timer < timeout) {
			final long startTime = System.currentTimeMillis();
			scriptingEngine.update(1f);
			scriptingEngine.skipAllRunningGameFutures();
			timer += System.currentTimeMillis() - startTime;
		}
		if(timer >= timeout) {
			Assert.fail("Timed out after " + timeout + "ms wait for script");
		}

		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(false, gameFuture.isUpdated());
		Assert.assertEquals(true, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isCompleted());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
		Assert.assertEquals(true, gameFuture.isReadyForGC());
		Assert.assertEquals(true, gameFuture.isFutureSkipped());
		Assert.assertEquals(true, gameFuture.isFutureSkippedCalled());
	}
	
	@Test
	public void testSkipScript() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getWaitForCompletionScript());
		final int taskId1 = scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings);
		final int taskId2 = scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {
			
			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				scriptResult.set(ScriptResult.SUCCESS);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}
			
			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		final long timeout = 20000L;
		long timer = 0L;
		while(!scriptExecuted.get() && timer < timeout) {
			long startTime = System.currentTimeMillis();
			scriptingEngine.update(1f);
			try {
				Thread.sleep(1000);
			} catch (Exception e) {}
			timer += System.currentTimeMillis() - startTime;

			if(timer >= 15000L) {
				scriptingEngine.skipScript(expectedScriptId);
			}
		}
		if(timer >= timeout) {
			Assert.fail("Timed out after " + timeout + "ms wait for script");
		}

		Assert.assertEquals(ScriptResult.SKIPPED, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(true, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isCompleted());
		Assert.assertEquals(true, gameFuture.isReadyForGC());
		Assert.assertEquals(true, gameFuture.isScriptSkipped());
		Assert.assertEquals(true, gameFuture.isScriptSkippedCalled());
		Assert.assertNotEquals(taskId1, taskId2);
	}

	@Test
	public void testSkipScriptByTaskId() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getWaitForCompletionScript());
		final int taskId1 = scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings);
		final int taskId2 = scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {

			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				scriptResult.set(ScriptResult.SUCCESS);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		final long timeout = 20000L;
		long timer = 0L;
		while(!scriptExecuted.get() && timer < timeout) {
			long startTime = System.currentTimeMillis();
			scriptingEngine.update(1f);
			try {
				Thread.sleep(1000);
			} catch (Exception e) {}
			timer += System.currentTimeMillis() - startTime;

			if(timer >= 5000L) {
				scriptingEngine.skipScriptByTaskId(taskId1);
			}
			if(timer >= 15000L) {
				scriptingEngine.skipScriptByTaskId(taskId2);
			}
		}
		if(timer >= timeout) {
			Assert.fail("Timed out after " + timeout + "ms wait for script");
		}

		Assert.assertEquals(ScriptResult.SKIPPED, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(true, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isFutureSkippedCalled());
		Assert.assertEquals(false, gameFuture.isCompleted());
		Assert.assertEquals(true, gameFuture.isReadyForGC());
		Assert.assertEquals(true, gameFuture.isScriptSkipped());
		Assert.assertEquals(true, gameFuture.isScriptSkippedCalled());

		scriptingEngine.skipScriptByTaskId(taskId1);
		Assert.assertNotEquals(taskId1, taskId2);
	}

	@Test
	public void testSkipScriptAlreadyCompleted() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getWaitForCompletionScript());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {

			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}

			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		final long timeout = 20000L;
		long timer = 0L;

		while(!scriptExecuted.get() && timer < timeout) {
			long startTime = System.currentTimeMillis();
			gameFuture.setFutureCompleted(true);
			scriptingEngine.update(1f);
			timer += System.currentTimeMillis() - startTime;
		}
		scriptingEngine.update(1f);

		if(timer >= timeout) {
			Assert.fail("Timed out after " + timeout + "ms wait for script");
		}

		scriptingEngine.skipScript(expectedScriptId);

		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(true, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
		Assert.assertEquals(true, gameFuture.isCompleted());
		Assert.assertEquals(1, gameFuture.getUpdateCount());
	}

	@Test
	public void testNonExistantScript() {
		scriptingEngine.invokeCompiledScript(-1, scriptBindings, new ScriptInvocationListener() {

			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				scriptResult.set(ScriptResult.SUCCESS);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});

		final long timeout = 20000L;
		long timer = 0L;

		while(!scriptExecuted.get() && timer < timeout) {
			try {
				Thread.sleep(1000);
			} catch (Exception e) {}
		}

		if(timer >= timeout) {
			Assert.fail("Timed out after " + timeout + "ms wait for script");
		}

		Assert.assertEquals(ScriptResult.EXCEPTION, scriptResult.get());
	}
	
	@Test
	public void testGameFutureGarbageCollection() throws Exception {
		final int expectedScriptId = scriptingEngine.compileScript(getWaitForCompletionScript());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {
			
			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}
			
			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}
			
			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}
			
			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		Assert.assertEquals(0, scriptingEngine.runningFutures.size());
		while(!scriptExecuted.get()) {
			scriptingEngine.update(1f);

			if(scriptingEngine.runningFutures.size() > 0) {
				gameFuture.setFutureCompleted(true);
			}
		}

		try {
			Thread.sleep(2000);
		} catch (Exception e) {}
		scriptingEngine.update(1f);
		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(0, scriptingEngine.runningFutures.size());
	}

	@Test
	public void testInvokeEmbeddedScript() throws Exception {
		if(!scriptingEngine.isEmbeddedSynchronousScriptSupported()) {
			return;
		}

		final int expectedScriptId = scriptingEngine.compileScript(getInvokeWithinScriptFilepath(), getInvokeWithScript());
		scriptingEngine.compileScript(getDefaultScriptFilepath(), getDefaultScript());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {

			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedEmbeddedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}

			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		final long timeout = 20000L;
		long timer = 0L;

		while(!scriptExecuted.get() && timer < timeout) {
			long startTime = System.currentTimeMillis();
			scriptingEngine.update(1f);
			timer += System.currentTimeMillis() - startTime;
		}

		if(timer >= timeout) {
			Assert.fail("Timed out after " + timeout + "ms wait for script");
		}

		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(false, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
	}

	@Test
	public void testInvokeNestedEmbeddedScript() throws Exception {
		if(!scriptingEngine.isEmbeddedSynchronousScriptSupported()) {
			return;
		}

		final int expectedScriptId = scriptingEngine.compileScript(getNestedInvokeWithinScriptFilepath(), getNestedInvokeWithScript());
		scriptingEngine.compileScript(getDefaultScriptFilepath(), getDefaultScript());
		scriptingEngine.compileScript(getInvokeWithinScriptFilepath(), getInvokeWithScript());
		scriptingEngine.invokeCompiledScript(expectedScriptId, scriptBindings, new ScriptInvocationListener() {

			@Override
			public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
				if(scriptId != expectedScriptId) {
					scriptResult.set(ScriptResult.INCORRECT_SCRIPT_ID);
					scriptExecuted.set(true);
				} else if(!checkExpectedNestedEmbeddedScriptResults(executionResult)) {
					scriptResult.set(ScriptResult.INCORRECT_VARIABLES);
					scriptExecuted.set(true);
				} else {
					scriptResult.set(ScriptResult.SUCCESS);
					scriptExecuted.set(true);
				}
			}

			@Override
			public void onScriptSkipped(int scriptId) {
				scriptResult.set(ScriptResult.SKIPPED);
				scriptExecuted.set(true);
			}

			@Override
			public void onScriptException(int scriptId, Exception e) {
				e.printStackTrace();
				scriptResult.set(ScriptResult.EXCEPTION);
				scriptExecuted.set(true);
			}

			@Override
			public boolean callOnGameThread() {
				return true;
			}
		});
		final long timeout = 20000L;
		long timer = 0L;

		while(!scriptExecuted.get() && timer < timeout) {
			long startTime = System.currentTimeMillis();
			scriptingEngine.update(1f);
			timer += System.currentTimeMillis() - startTime;
		}

		if(timer >= timeout) {
			Assert.fail("Timed out after " + timeout + "ms wait for script");
		}

		Assert.assertEquals(ScriptResult.SUCCESS, scriptResult.get());
		Assert.assertEquals(true, gameFuture.isUpdated());
		Assert.assertEquals(false, gameFuture.waitOccurred());
		Assert.assertEquals(false, gameFuture.isFutureSkipped());
		Assert.assertEquals(false, gameFuture.isScriptSkipped());
	}

	protected abstract GameScriptingEngine createScriptingEngine();
	
	protected abstract InputStream getDefaultScriptInputStream();
	
	protected abstract String getDefaultScript();

	protected abstract String getDefaultScriptFilepath();

	protected abstract String getInvokeWithScript();

	protected abstract String getInvokeWithinScriptFilepath();

	protected abstract String getNestedInvokeWithScript();

	protected abstract String getNestedInvokeWithinScriptFilepath();

	protected abstract String getWaitForCompletionScript();

	protected boolean checkExpectedNestedEmbeddedScriptResults(ScriptExecutionResult executionResult) {
		if(!executionResult.containsKey("booleanValue")) {
			System.err.println("booleanValue not present");
			return false;
		}
		if(!executionResult.containsKey("intValue")) {
			System.err.println("intValue not present");
			return false;
		}
		if(!"hello1234".equals(executionResult.get("stringValue"))) {
			System.err.println("Expected stringValue to be hello123 but was " + executionResult.get("stringValue"));
			return false;
		}
		if(!executionResult.containsKey("intValue2")) {
			System.err.println("intValue2 not present");
			return false;
		}
		if(executionResult.get("intValue2") instanceof Integer && ((Integer) executionResult.get("intValue2")) != 102) {
			System.err.println("Expected intValue2 to be 102 but was " + executionResult.get("intValue2"));
			return false;
		}
		return true;
	}

	protected boolean checkExpectedEmbeddedScriptResults(ScriptExecutionResult executionResult) {
		if(!checkExpectedScriptResults(executionResult)) {
			return false;
		}
		if(!executionResult.containsKey("intValue2")) {
			System.err.println("intValue2 not present");
			return false;
		}
		if(executionResult.get("intValue2") instanceof Integer && ((Integer) executionResult.get("intValue2")) != 102) {
			System.err.println("Expected intValue2 to be 102 but was " + executionResult.get("intValue2"));
			return false;
		}
		return true;
	}
	
	protected boolean checkExpectedScriptResults(ScriptExecutionResult executionResult) {
		if(!executionResult.containsKey("stringValue")) {
			System.err.println("stringValue not present");
			return false;
		}
		if(!executionResult.containsKey("booleanValue")) {
			System.err.println("booleanValue not present");
			return false;
		}
		if(!executionResult.containsKey("intValue")) {
			System.err.println("intValue not present");
			return false;
		}
		if(!"hello123".equals(executionResult.get("stringValue"))) {
			System.err.println("Expected stringValue to be hello123 but was " + executionResult.get("stringValue"));
			return false;
		}
		if(!((Boolean) executionResult.get("booleanValue"))) {
			System.err.println("Expected booleanValue to be true but was " + executionResult.get("stringValue"));
			return false;
		}
		//Most languages use Integer
		if(executionResult.get("intValue") instanceof Integer && ((Integer) executionResult.get("intValue")) != 101) {
			System.err.println("Expected intValue to be 101 but was " + executionResult.get("intValue"));
			return false;
		}
		//Ruby uses Long
		if(executionResult.get("intValue") instanceof Long && ((Long) executionResult.get("intValue")) != 101) {
			System.err.println("Expected intValue to be 101 but was " + executionResult.get("intValue"));
			return false;
		}
		return true;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/threadpool/ScheduledTask.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core.threadpool;

import org.mini2Dx.miniscript.core.util.ReadWriteArrayQueue;

import java.util.concurrent.TimeUnit;

public class ScheduledTask implements Comparable<ScheduledTask> {
	private static final ReadWriteArrayQueue<ScheduledTask> POOL = new ReadWriteArrayQueue<>();

	private ScheduledTaskFuture future;

	private Runnable runnable;
	private long scheduledStartTimeNanos;

	private long repeatInterval;
	private TimeUnit repeatUnit;

	private boolean disposed = false;

	public void dispose() {
		if(disposed) {
			return;
		}

		disposed = true;

		runnable = null;
		scheduledStartTimeNanos = 0L;

		repeatUnit = null;
		repeatInterval = 0L;

		future = null;

		POOL.add(this);
	}

	public static ScheduledTask allocate(Runnable runnable, long scheduledStartTimeNanos) {
		ScheduledTask task = POOL.poll();
		if(task == null) {
			task = new ScheduledTask();
		}
		task.disposed = false;
		task.runnable = runnable;
		task.scheduledStartTimeNanos = scheduledStartTimeNanos;
		return task;
	}

	public static ScheduledTask allocate(Runnable runnable, long scheduledStartTimeNanos, long repeatInterval, TimeUnit repeatUnit) {
		ScheduledTask task = POOL.poll();
		if(task == null) {
			task = new ScheduledTask();
		}
		task.disposed = false;
		task.runnable = runnable;
		task.scheduledStartTimeNanos = scheduledStartTimeNanos;
		task.repeatUnit = repeatUnit;
		task.repeatInterval = repeatInterval;
		return task;
	}

	public Runnable getRunnable() {
		return runnable;
	}

	public long getScheduledStartTimeNanos() {
		return scheduledStartTimeNanos;
	}

	public boolean isRepeating() {
		return repeatUnit != null;
	}

	public long getRepeatInterval() {
		return repeatInterval;
	}

	public TimeUnit getRepeatUnit() {
		return repeatUnit;
	}

	@Override
	public int compareTo(ScheduledTask o) {
		return Long.compare(scheduledStartTimeNanos, o.scheduledStartTimeNanos);
	}

	public ScheduledTaskFuture getFuture() {
		return future;
	}

	public void setFuture(ScheduledTaskFuture future) {
		this.future = future;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/EmbeddedScriptInvoker.java<|end_filename|>
/**
 * The MIT License (MIT)
 * <p>
 * Copyright (c) 2020 <NAME>
 * <p>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * <p>
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * <p>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import org.mini2Dx.miniscript.core.exception.NoSuchScriptException;

/**
 * Utility class for invoking scripts inside of other scripts
 */
public abstract class EmbeddedScriptInvoker {
	protected final GameScriptingEngine gameScriptingEngine;

	protected int parentScriptId;
	protected ScriptBindings scriptBindings;

	public EmbeddedScriptInvoker(GameScriptingEngine gameScriptingEngine) {
		this.gameScriptingEngine = gameScriptingEngine;
	}

	public int getScriptId(String filepath) {
		return gameScriptingEngine.getCompiledScriptId(filepath);
	}

	/**
	 * Invokes a script during the execution of the current script. Equivalent to calling a function.
	 * @param scriptId The script ID to invoke
	 */
	public abstract void invokeSync(int scriptId);

	/**
	 * Invokes a script during the execution of the current script. Equivalent to calling a function.
	 * @param filepath The filepath of the script to invoke
	 */
	public void invokeSync(String filepath) {
		final int scriptId = gameScriptingEngine.getCompiledScriptId(filepath);
		if (scriptId < 0) {
			throw new NoSuchScriptException(filepath);
		}
		invokeSync(scriptId);
	}

	/**
	 * Queues a script for invocation in the game engine. Depending on the number of script threads
	 * and currently excuting scripts, this may invoke simultaneously to the current script.
	 * @param scriptId The script ID to invoke
	 */
	public void invokeAsync(int scriptId) {
		gameScriptingEngine.invokeCompiledScript(scriptId, scriptBindings.duplicate());
	}

	/**
	 * Queues a script for invocation in the game engine. Depending on the number of script threads
	 * and currently excuting scripts, this may invoke simultaneously to the current script.
	 * @param filepath The filepath of the script to invoke
	 */
	public void invokeAsync(String filepath) {
		final int scriptId = gameScriptingEngine.getCompiledScriptId(filepath);
		if (scriptId < 0) {
			throw new NoSuchScriptException(filepath);
		}
		invokeAsync(scriptId);
	}

	public void setScriptBindings(ScriptBindings scriptBindings) {
		this.scriptBindings = scriptBindings;
	}

	public void setParentScriptId(int parentScriptId) {
		this.parentScriptId = parentScriptId;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/ScriptInvocationPool.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import org.mini2Dx.miniscript.core.util.ReadWriteArrayQueue;

import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Provides a pool of reusable {@link ScriptInvocation} instances to reduce
 * object allocation
 */
public class ScriptInvocationPool {
	private final AtomicInteger ID_GENERATOR = new AtomicInteger(0);
	private final Queue<ScriptInvocation> pool = new ReadWriteArrayQueue<>();
	
	/**
	 * Allocate a {@link ScriptInvocation} instance
	 * @param scriptId The script id being invoked
	 * @param scriptBindings The {@link ScriptBindings}
	 * @param invocationListener An optional {@link ScriptInvocationListener}
	 * @param priority The script execution priority
	 * @return A {@link ScriptInvocation} instance set to the provided parameters
	 */
	public ScriptInvocation allocate(int scriptId, ScriptBindings scriptBindings, ScriptInvocationListener invocationListener,
	                                 int priority, boolean interactive) {
		ScriptInvocation result = pool.poll();
		if(result == null) {
			//If pool is empty create a new instance that will eventually be returned to the pool
			result = new ScriptInvocation(this);
		}
		result.setScriptId(scriptId);
		result.setScriptBindings(scriptBindings);
		result.setInvocationListener(invocationListener);
		result.setPriority(priority);
		result.setInteractive(interactive);
		result.setInvokeTimestamp(System.nanoTime());
		result.setTaskId(ID_GENERATOR.incrementAndGet());
		return result;
	}
	
	/**
	 * Releases a {@link ScriptInvocation} back to the pool
	 * @param scriptInvocation The {@link ScriptInvocation} to release
	 */
	public void release(ScriptInvocation scriptInvocation) {
		pool.offer(scriptInvocation);
	}
}


<|start_filename|>gradle-plugin/src/main/java/org/mini2Dx/miniscript/gradle/compiler/LuaScriptCompiler.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.gradle.compiler;

import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.luaj.vm2.lib.jse.JsePlatform;
import org.mini2Dx.miniscript.gradle.CompilerInputFile;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;

public class LuaScriptCompiler implements ScriptCompiler {
	private final Project project;

	public LuaScriptCompiler(Project project) {
		super();
		this.project = project;
	}

	@Override
	public Object compileFile(CompilerConfig compilerConfig) throws IOException {
		final CompilerInputFile inputFile = compilerConfig.getInputScriptFile();
		final String luaChunkName = compilerConfig.getOutputPackageAsPath() + inputFile.getOutputClassName();
		FileInputStream fileInputStream = new FileInputStream( inputFile.getInputScriptFile() );
		final Hashtable luaHashtable = PatchedLuaJC.instance.compileAll( fileInputStream, luaChunkName,
				compilerConfig.getOutputPackageAsPath() + inputFile.getOutputClassName(),
				JsePlatform.standardGlobals(), false);
		fileInputStream.close();

		for (Enumeration enumeration = luaHashtable.keys(); enumeration.hasMoreElements(); ) {
			final String className = (String) enumeration.nextElement();
			final byte[] bytes = (byte[]) luaHashtable.get(className);
			final FileOutputStream outputStream = new FileOutputStream(compilerConfig.getOutputClassFile(className));
			outputStream.write( bytes );
			outputStream.close();
		}

		final LuaClassLoader luaClassLoader = new LuaClassLoader(luaHashtable, project);
		for ( Enumeration enumeration = luaHashtable.keys(); enumeration.hasMoreElements(); ) {
			final String className = (String) enumeration.nextElement();
			try {
				final Class clazz = luaClassLoader.loadClass(className);
				return clazz.newInstance();
			} catch ( Exception ex ) {
				throw new GradleException("Failed to load " + className);
			}
		}
		return null;
	}

	private static final class LuaClassLoader extends ClassLoader {
		private final Hashtable luaHashtable;
		private final Project project;
		private final Set<String> loadedClasses = new HashSet<>();

		private LuaClassLoader(Hashtable luaHashtable, Project project) {
			this.luaHashtable = luaHashtable;
			this.project = project;
		}

		public Class findClass(String classname) throws ClassNotFoundException {
			final String classNameWithDot = classname.replace('/', '.');
			final String classNameWithSlash = classname.replace('.', '/');

			if(loadedClasses.contains(classNameWithSlash)) {
				return findLoadedClass(classNameWithDot);
			}
			byte[] bytes = (byte[]) luaHashtable.get(classNameWithSlash);
			final Class result;
			if (bytes != null) {
				result = defineClass(classNameWithDot, bytes, 0, bytes.length);
			} else {
				result = project.getBuildscript().getClassLoader().loadClass(classNameWithDot);
			}
			loadedClasses.add(classNameWithSlash);
			return result;
		}
	}
}


<|start_filename|>python/src/main/java/org/mini2Dx/miniscript/python/PythonScriptExecutor.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.python;

import org.mini2Dx.miniscript.core.*;
import org.mini2Dx.miniscript.core.exception.ScriptSkippedException;
import org.python.core.*;
import org.python.util.InteractiveInterpreter;

/**
 * An implementation of {@link ScriptExecutor} for Python-based scripts
 */
public class PythonScriptExecutor implements ScriptExecutor<PyCode> {
	private final PythonScriptExecutorPool executorPool;
	private final InteractiveInterpreter pythonInterpreter;

	public PythonScriptExecutor(PythonScriptExecutorPool executorPool) {
		this.executorPool = executorPool;

		pythonInterpreter = new InteractiveInterpreter();
		pythonInterpreter.setErr(System.err);
		pythonInterpreter.setOut(System.out);
	}

	@Override
	public GameScript<PyCode> compile(String script) {
		return new GlobalGameScript<PyCode>(pythonInterpreter.compile(script));
	}

	@Override
	public ScriptExecutionResult execute(int scriptId, GameScript<PyCode> script, ScriptBindings bindings, boolean returnResult) throws Exception {
		final PyCode pythonScript = script.getScript();

		final PythonEmbeddedScriptInvoker embeddedScriptInvoker = executorPool.getEmbeddedScriptInvokerPool().allocate();
		embeddedScriptInvoker.setScriptBindings(bindings);
		embeddedScriptInvoker.setScriptExecutor(this);
		embeddedScriptInvoker.setParentScriptId(scriptId);

		for (String variableName : bindings.keySet()) {
			pythonInterpreter.set(variableName, bindings.get(variableName));
		}
		pythonInterpreter.set(ScriptBindings.SCRIPT_PARENT_ID_VAR, -1);
		pythonInterpreter.set(ScriptBindings.SCRIPT_ID_VAR, scriptId);
		pythonInterpreter.set(ScriptBindings.SCRIPT_INVOKE_VAR, embeddedScriptInvoker);

		try {
			pythonInterpreter.exec(pythonScript);
		} catch (PyException e) {
			if(e.getCause() instanceof ScriptSkippedException) {
				throw new ScriptSkippedException();
			} else {
				throw e;
			}
		}

		executorPool.getEmbeddedScriptInvokerPool().release(embeddedScriptInvoker);
		
		if(!returnResult) {
			return null;
		}
		final ScriptExecutionResult executionResult = new ScriptExecutionResult(null);

		final PyStringMap  locals = (PyStringMap) pythonInterpreter.getLocals();
		for(Object key : locals.keys()) {
			executionResult.put(key.toString(), pythonInterpreter.get(key.toString(), Object.class));
		}
		return executionResult;
	}

	@Override
	public void executeEmbedded(int parentScriptId, int scriptId, GameScript<PyCode> script,
								EmbeddedScriptInvoker embeddedScriptInvoker, ScriptBindings bindings) throws Exception {
		final PyCode pythonScript = script.getScript();

		pythonInterpreter.set(ScriptBindings.SCRIPT_PARENT_ID_VAR, parentScriptId);
		pythonInterpreter.set(ScriptBindings.SCRIPT_ID_VAR, scriptId);
		embeddedScriptInvoker.setParentScriptId(scriptId);

		try {
			pythonInterpreter.exec(pythonScript);
		} catch (Exception e) {
			if(e instanceof ScriptSkippedException || e.getCause() instanceof ScriptSkippedException) {
				throw new ScriptSkippedException();
			} else {
				throw e;
			}
		}

		pythonInterpreter.set(ScriptBindings.SCRIPT_ID_VAR, parentScriptId);
		embeddedScriptInvoker.setParentScriptId(parentScriptId);

		final PyStringMap  locals = (PyStringMap) pythonInterpreter.getLocals();
		for(Object key : locals.keys()) {
			bindings.put(key.toString(), pythonInterpreter.get(key.toString(), Object.class));
		}
	}

	@Override
	public void release() {
		try {
			pythonInterpreter.cleanup();
		} catch (Exception e) {
			e.printStackTrace();
		}
		executorPool.release(this);
	}

}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/util/ToCollectionConsumer.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import java.util.Collection;
import java.util.function.Consumer;

public class ToCollectionConsumer<T> implements Consumer<T> {
	private Collection<T> collection;

	@Override
	public void accept(T t) {
		collection.add(t);
	}

	public void setCollection(Collection<T> list) {
		this.collection = list;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/InteractiveScriptListener.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core;

import org.mini2Dx.miniscript.core.notification.ScriptBeginNotification;
import org.mini2Dx.miniscript.core.notification.ScriptExceptionNotification;
import org.mini2Dx.miniscript.core.notification.ScriptSkippedNotification;
import org.mini2Dx.miniscript.core.notification.ScriptSuccessNotification;
import org.mini2Dx.miniscript.core.util.ScriptInvocationQueue;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class InteractiveScriptListener implements ScriptInvocationListener {
	/**
	 * When true sends notifications then allows next interaction script.
	 * Defaults to false which allows next interaction script then notifies.
	 */
	public static boolean NOTIFY_THEN_ALLOW_INTERACTION = false;
	/**
	 * When true waits for game thread notifications to be processed before proceeding
	 */
	public static boolean WAIT_FOR_GAME_THREAD_NOTIFICATIONS = false;

	private final AtomicInteger scriptId = new AtomicInteger();
	private final AtomicReference<ScriptInvocationListener> invocationListener = new AtomicReference<>();

	private final GameScriptingEngine scriptingEngine;
	private final ScriptInvocationQueue invocationQueue;

	public InteractiveScriptListener(GameScriptingEngine scriptingEngine, ScriptInvocationQueue invocationQueue) {
		this.scriptingEngine = scriptingEngine;
		this.invocationQueue = invocationQueue;
	}

	public void track(int scriptId, ScriptInvocationListener invocationListener) {
		this.scriptId.set(scriptId);
		this.invocationListener.set(invocationListener);
	}

	@Override
	public void onScriptBegin(int scriptId) {
		final ScriptInvocationListener invocationListener = this.invocationListener.get();
		if(scriptId != this.scriptId.get()) {
			return;
		}
		if(invocationListener == null) {
			return;
		}
		if (invocationListener.callOnGameThread()) {
			scriptingEngine.scriptNotifications.offer(
					new ScriptBeginNotification(invocationListener, scriptId));
		} else {
			invocationListener.onScriptBegin(scriptId);
		}
	}

	@Override
	public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
		final ScriptInvocationListener invocationListener = this.invocationListener.get();
		if(scriptId != this.scriptId.get()) {
			return;
		}
		clearInteractionStatusBeforeNotification();

		try {
			if (invocationListener != null) {
				if (invocationListener.callOnGameThread()) {
					final ScriptSuccessNotification notification = new ScriptSuccessNotification(
							invocationListener, scriptId, executionResult);
					scriptingEngine.scriptNotifications.offer(notification);
					if(WAIT_FOR_GAME_THREAD_NOTIFICATIONS) {
						notification.waitForNotification();
					}
				} else {
					invocationListener.onScriptSuccess(scriptId, executionResult);
				}
			}
		} finally {
			clearInteractionStatusAfterNotification();
		}
	}

	@Override
	public void onScriptSkipped(int scriptId) {
		final ScriptInvocationListener invocationListener = this.invocationListener.get();
		if(scriptId != this.scriptId.get()) {
			return;
		}
		clearInteractionStatusBeforeNotification();

		try {
			if (invocationListener != null) {
				if (invocationListener.callOnGameThread()) {
					final ScriptSkippedNotification notification =
							new ScriptSkippedNotification(invocationListener, scriptId);
					scriptingEngine.scriptNotifications.offer(notification);
					if(WAIT_FOR_GAME_THREAD_NOTIFICATIONS) {
						notification.waitForNotification();
					}
				} else {
					invocationListener.onScriptSkipped(scriptId);
				}
			}
		} finally {
			clearInteractionStatusAfterNotification();
		}
	}

	@Override
	public void onScriptException(int scriptId, Exception e) {
		final ScriptInvocationListener invocationListener = this.invocationListener.get();
		if(scriptId != this.scriptId.get()) {
			return;
		}
		clearInteractionStatusBeforeNotification();

		try {
			if (invocationListener != null) {
				if (invocationListener.callOnGameThread()) {
					ScriptExceptionNotification notification =
							new ScriptExceptionNotification(invocationListener, scriptId, e);
					scriptingEngine.scriptNotifications.offer(notification);
					if(WAIT_FOR_GAME_THREAD_NOTIFICATIONS) {
						notification.waitForNotification();
					}
				} else {
					invocationListener.onScriptException(scriptId, e);
				}
			} else {
				e.printStackTrace();
			}
		} finally {
			clearInteractionStatusAfterNotification();
		}
	}

	private void clearInteractionStatusBeforeNotification() {
		if(NOTIFY_THEN_ALLOW_INTERACTION) {
			return;
		}
		invocationQueue.clearInteractiveScriptStatus();
	}

	private void clearInteractionStatusAfterNotification() {
		if(!NOTIFY_THEN_ALLOW_INTERACTION) {
			return;
		}
		invocationQueue.clearInteractiveScriptStatus();
	}

	public ScriptInvocationListener getInvocationListener() {
		return invocationListener.get();
	}

	@Override
	public boolean callOnGameThread() {
		return false;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/notification/ScriptCancelledNotification.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.notification;

import org.mini2Dx.miniscript.core.ScriptInvocationListener;

public class ScriptCancelledNotification implements ScriptNotification {
	private final ScriptInvocationListener invocationListener;
	private final int scriptId;

	public ScriptCancelledNotification(ScriptInvocationListener invocationListener, int scriptId) {
		this.invocationListener = invocationListener;
		this.scriptId = scriptId;
	}

	@Override
	public void process() {
		invocationListener.onScriptCancelled(scriptId);
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/util/AbstractConcurrentBlockingQueue.java<|end_filename|>
/**
 * Copyright 2020 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class AbstractConcurrentBlockingQueue<E> extends AbstractConcurrentQueue<E> implements BlockingQueue<E> {
	private final int maxCapacity;
	protected final Object waitingForItemsMonitor = new Object();
	protected final Object waitingForRemovalMonitor = new Object();

	public AbstractConcurrentBlockingQueue(int maxCapacity, Queue<E> internalQueue) {
		super(internalQueue);
		this.maxCapacity = maxCapacity;
	}

	@Override
	public E poll() {
		final E result = super.poll();
		synchronized(waitingForRemovalMonitor) {
			waitingForRemovalMonitor.notify();
		}
		return result;
	}

	@Override
	public boolean remove(Object o) {
		final boolean result = super.remove(o);
		synchronized(waitingForRemovalMonitor) {
			waitingForRemovalMonitor.notify();
		}
		return result;
	}

	@Override
	public boolean removeAll(Collection<?> c) {
		final boolean result = super.removeAll(c);
		synchronized(waitingForRemovalMonitor) {
			waitingForRemovalMonitor.notify();
		}
		return result;
	}

	@Override
	public boolean offer(E e) {
		final boolean result = super.offer(e);
		synchronized(waitingForItemsMonitor) {
			waitingForItemsMonitor.notify();
		}
		return result;
	}

	@Override
	public boolean add(E e) {
		final boolean result = super.add(e);
		synchronized(waitingForItemsMonitor) {
			waitingForItemsMonitor.notify();
		}
		return result;
	}

	@Override
	public boolean addAll(Collection<? extends E> c) {
		final boolean result = super.addAll(c);
		synchronized(waitingForItemsMonitor) {
			waitingForItemsMonitor.notify();
		}
		return result;
	}

	@Override
	public void put(E e) throws InterruptedException {
		lock.lockWrite();
		while(size() >= maxCapacity) {
			lock.unlockWrite();
			synchronized(waitingForRemovalMonitor) {
				waitingForRemovalMonitor.wait();
			}
			lock.lockWrite();
		}
		super.offer(e);
		lock.unlockWrite();

		synchronized(waitingForItemsMonitor) {
			waitingForItemsMonitor.notify();
		}
	}

	@Override
	public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
		lock.lockWrite();
		if(size() >= maxCapacity) {
			lock.unlockWrite();
			synchronized(waitingForRemovalMonitor) {
				waitingForRemovalMonitor.wait(0L, (int) unit.toNanos(timeout));
			}
			lock.lockWrite();
		}
		if(size() >= maxCapacity) {
			lock.unlockWrite();
			return false;
		}
		offer(e);
		lock.unlockWrite();
		return true;
	}

	@Override
	public E take() throws InterruptedException {
		lock.lockWrite();
		while (isEmpty()) {
			lock.unlockWrite();
			synchronized(waitingForItemsMonitor) {
				waitingForItemsMonitor.wait();
			}
			lock.lockWrite();
		}
		final E result = super.remove();
		lock.unlockWrite();

		synchronized(waitingForRemovalMonitor) {
			waitingForRemovalMonitor.notify();
		}
		return result;
	}

	@Override
	public E poll(long timeout, TimeUnit unit) throws InterruptedException {
		lock.lockWrite();
		if (isEmpty()) {
			lock.unlockWrite();
			synchronized(waitingForItemsMonitor) {
				waitingForItemsMonitor.wait(0L, (int) unit.toNanos(timeout));
			}
			lock.lockWrite();
		}
		final E result = poll();
		lock.unlockWrite();

		synchronized(waitingForRemovalMonitor) {
			waitingForRemovalMonitor.notify();
		}
		return result;
	}

	@Override
	public int remainingCapacity() {
		return maxCapacity - size();
	}

	@Override
	public int drainTo(Collection<? super E> c) {
		throw new UnsupportedOperationException();
	}

	@Override
	public int drainTo(Collection<? super E> c, int maxElements) {
		throw new UnsupportedOperationException();
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/ScriptInvocationListener.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

/**
 * Interface for script execution listeners.
 * 
 * Note: All callbacks occur on a different thread from the game thread
 */
public interface ScriptInvocationListener {
	/**
	 * Called just before a script begins execution. Note: If callOnGameThread() is true,
	 * the script will not begin until this callback is processed on the game thread
	 * @param scriptId The script id
	 */
	public default void onScriptBegin(int scriptId) {

	}

	/**
	 * Called when a script is cancelled while in the invoke queue.
	 *
	 * @param scriptId The script id
	 */
	public default void onScriptCancelled(int scriptId) {
	}

	/**
	 * Called when a script successfully completes
	 * 
	 * @param scriptId
	 *            The script id
	 * @param executionResult
	 *            The variable bindings after execution
	 */
	public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult);

	/**
	 * Called when a script is skipped during execution
	 * 
	 * @param scriptId
	 *            The script id
	 */
	public void onScriptSkipped(int scriptId);

	/**
	 * Called when an exception occurs during script execution
	 * 
	 * @param scriptId
	 *            The script id
	 * @param e
	 *            The exception that occurred
	 */
	public void onScriptException(int scriptId, Exception e);

	/**
	 * Returns if this {@link ScriptInvocationListener} should be notified on
	 * the game thread
	 * 
	 * @return False if this should be notified on the
	 *         {@link GameScriptingEngine} thread pool
	 */
	public boolean callOnGameThread();
}


<|start_filename|>lua/src/test/resources/nestedInvokeWithinScript.lua<|end_filename|>
scripts:invokeSync("invokeWithinScript.lua")
stringValue = stringValue .. "4"

<|start_filename|>ruby/src/main/java/org/mini2Dx/miniscript/ruby/RubyScriptExecutor.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.ruby;

import org.jruby.embed.EmbedEvalUnit;
import org.jruby.embed.ScriptingContainer;
import org.mini2Dx.miniscript.core.*;
import org.mini2Dx.miniscript.core.exception.ScriptSkippedException;

/**
 * An implementation of {@link ScriptExecutor} for Ruby-based scripts
 */
public class RubyScriptExecutor implements ScriptExecutor<EmbedEvalUnit> {
	private final RubyScriptExecutorPool executorPool;

	public RubyScriptExecutor(RubyScriptExecutorPool executorPool) {
		this.executorPool = executorPool;
	}

	@Override
	public GameScript<EmbedEvalUnit> compile(String script) {
		return new PerThreadGameScript<EmbedEvalUnit>(script);
	}

	@Override
	public ScriptExecutionResult execute(int scriptId, GameScript<EmbedEvalUnit> s, ScriptBindings bindings,
			boolean returnResult) throws Exception {
		final PerThreadGameScript<EmbedEvalUnit> script = (PerThreadGameScript<EmbedEvalUnit>) s;

		final ScriptingContainer scriptingContainer = executorPool.getLocalScriptingContainer();

		final RubyEmbeddedScriptInvoker embeddedScriptInvoker = executorPool.getEmbeddedScriptInvokerPool().allocate();
		embeddedScriptInvoker.setScriptBindings(bindings);
		embeddedScriptInvoker.setScriptExecutor(this);
		embeddedScriptInvoker.setParentScriptId(scriptId);

		scriptingContainer.getVarMap().putAll(bindings);
		scriptingContainer.getVarMap().put(ScriptBindings.SCRIPT_PARENT_ID_VAR, -1);
		scriptingContainer.getVarMap().put(ScriptBindings.SCRIPT_ID_VAR, scriptId);
		scriptingContainer.getVarMap().put(ScriptBindings.SCRIPT_INVOKE_VAR, embeddedScriptInvoker);

		if (!script.hasScript()) {
			script.setScript(scriptingContainer.parse(script.getContent()));
		}
		
		try {
			EmbedEvalUnit embedEvalUnit = script.getScript();
			embedEvalUnit.run();
		} catch (Exception e) {
			if(e instanceof ScriptSkippedException || e.getCause() instanceof ScriptSkippedException) {
				throw new ScriptSkippedException();
			} else {
				throw e;
			}
		}

		executorPool.getEmbeddedScriptInvokerPool().release(embeddedScriptInvoker);

		if (!returnResult) {
			scriptingContainer.clear();
			return null;
		}		
		ScriptExecutionResult executionResult = new ScriptExecutionResult(scriptingContainer.getVarMap().getMap());
		scriptingContainer.clear();
		return executionResult;
	}

	@Override
	public void executeEmbedded(int parentScriptId, int scriptId, GameScript<EmbedEvalUnit> s,
								EmbeddedScriptInvoker embeddedScriptInvoker, ScriptBindings bindings) throws Exception {
		throw new RuntimeException("Embedded synchronous script invokes not supported in Ruby.");
		/*final PerThreadGameScript<EmbedEvalUnit> script = (PerThreadGameScript<EmbedEvalUnit>) s;

		final ScriptingContainer scriptingContainer = executorPool.getLocalScriptingContainer();
		scriptingContainer.getVarMap().put(ScriptBindings.SCRIPT_PARENT_ID_VAR, parentScriptId);
		scriptingContainer.getVarMap().put(ScriptBindings.SCRIPT_ID_VAR, scriptId);
		embeddedScriptInvoker.setParentScriptId(scriptId);

		if (!script.hasScript()) {
			script.setScript(scriptingContainer.parse(script.getContent()));
		}

		try {
			EmbedEvalUnit embedEvalUnit = script.getScript();
			embedEvalUnit.run();
		} catch (Exception e) {
			if(e.getCause() instanceof ScriptSkippedException) {
				throw new ScriptSkippedException();
			} else {
				throw e;
			}
		}

		scriptingContainer.getVarMap().put(ScriptBindings.SCRIPT_ID_VAR, parentScriptId);
		embeddedScriptInvoker.setParentScriptId(parentScriptId);*/
	}

	@Override
	public void release() {
		executorPool.release(this);
	}

}


<|start_filename|>gradle-plugin/src/main/java/org/mini2Dx/miniscript/gradle/MiniscriptCompileTask.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.gradle;

import org.gradle.api.GradleException;
import org.mini2Dx.miniscript.gradle.compiler.CompilerConfig;
import org.mini2Dx.miniscript.gradle.compiler.LuaScriptCompiler;
import org.mini2Dx.miniscript.gradle.compiler.ScriptCompiler;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MiniscriptCompileTask extends MiniscriptTask {
	private final Map<String, ScriptCompiler> compilers = new HashMap<String, ScriptCompiler>();

	public MiniscriptCompileTask() {
		super();
		compilers.put(".lua", new LuaScriptCompiler(getProject()));
	}

	@Override
	protected void process(List<CompilerInputFile> inputFiles) throws IOException {
		final CompilerConfig compilerConfig = new CompilerConfig(outputPackage, outputDir);
		compileDirectory(compilerConfig, inputFiles);
	}

	private Map<String, Object> compileDirectory(CompilerConfig compilerConfig, List<CompilerInputFile> inputFiles) throws IOException {
		final Map<String, Object> outputClasses = new HashMap<String, Object> ();
		for(CompilerInputFile inputFile : inputFiles) {
			compilerConfig.setInputScriptFile(inputFile);
			outputClasses.put(inputFile.getInputScriptRelativeFilename(), compileFile(compilerConfig));
		}
		return outputClasses;
	}

	private Object compileFile(CompilerConfig compilerConfig) throws IOException {
		final ScriptCompiler compiler = compilers.get(compilerConfig.getInputScriptFile().getInputScriptFileSuffix());
		if(compiler == null) {
			throw new GradleException("miniscript does not support build time compilation of "
					+ compilerConfig.getInputScriptFile().getInputScriptFileSuffix() + " files.");
		}
		return compiler.compileFile(compilerConfig);
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/ScriptExecutorPool.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import org.mini2Dx.miniscript.core.exception.InsufficientCompilersException;

/**
 * Common interface for language-specific {@link ScriptExecutor} pools.
 * 
 * Manages resources for compilation and execution of scripts.
 */
public interface ScriptExecutorPool<S> {

	public int getCompiledScriptId(String filepath);

	public String getCompiledScriptPath(int scriptId);

	public int preCompileScript(String filepath, String scriptContent) throws InsufficientCompilersException;

	public ScriptExecutionTask<?> execute(int taskId, int scriptId, ScriptBindings scriptBindings,
			ScriptInvocationListener invocationListener, boolean syncCall);

	public void release(ScriptExecutor<S> executor);
	
	public GameScriptingEngine getGameScriptingEngine();
}


<|start_filename|>gradle-plugin/src/main/java/org/mini2Dx/miniscript/gradle/compiler/CompilerConfig.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.gradle.compiler;

import org.mini2Dx.miniscript.gradle.CompilerInputFile;

import java.io.File;

public class CompilerConfig {
	private final File rootOutputDir;
	private final String outputRootPackage;
	private final String outputRootPackageAsPath;
	private final File outputRootDirectory;

	private CompilerInputFile inputScriptFile;

	private String outputPackage, outputPackageAsPath;
	private File packageDir;

	public CompilerConfig(String outputRootPackage, File rootOutputDir) {
		super();
		this.rootOutputDir = rootOutputDir;
		this.outputRootPackage = outputRootPackage;
		this.outputRootPackageAsPath = outputRootPackage.replace('.', '/') + "/";
		this.outputRootDirectory = new File(rootOutputDir, outputRootPackageAsPath.replace('/', File.separatorChar));
	}

	public void setInputScriptFile(CompilerInputFile inputScriptFile) {
		this.inputScriptFile  = inputScriptFile;

		this.outputPackage = inputScriptFile.getOutputPackageName(outputRootPackage);
		this.outputPackageAsPath = outputPackage.replace('.', '/') + "/";

		packageDir = new File(rootOutputDir, outputPackage.replace('.', File.separatorChar));
		if(!packageDir.exists()) {
			packageDir.mkdirs();
		}
	}

	public String getOutputPackage() {
		return outputPackage;
	}

	public String getOutputPackageAsPath() {
		return outputPackageAsPath;
	}

	public CompilerInputFile getInputScriptFile() {
		return inputScriptFile;
	}

	public File getOutputClassFile(String qualifiedClassName) {
		final String className;
		if(qualifiedClassName.contains("/")) {
			className = qualifiedClassName.substring(qualifiedClassName.lastIndexOf('/') + 1).replace('-', '_');
		} else {
			className = qualifiedClassName.replace('-', '_');
		}
		return new File(packageDir,  className + ".class");
	}

	public String getOutputRootPackage() {
		return outputRootPackage;
	}

	public String getOutputRootPackageAsPath() {
		return outputRootPackageAsPath;
	}

	public File getOutputRootDirectory() {
		return outputRootDirectory;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/util/ReadWriteMap.java<|end_filename|>
/**
 * Copyright 2020 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import org.mini2Dx.lockprovider.ReadWriteLock;
import org.mini2Dx.miniscript.core.GameScriptingEngine;

import java.util.*;

public class ReadWriteMap<K, V> implements Map<K, V> {
	private final ReadWriteLock lock = GameScriptingEngine.LOCK_PROVIDER.newReadWriteLock();
	private final Map<K, V> internalMap = new HashMap<>();

	private final ThreadLocal<Set<K>> keySet = new ThreadLocal<Set<K>>() {
		@Override
		protected Set<K> initialValue() {
			return new HashSet<>();
		}
	};
	private final ThreadLocal<Collection<V>> values = new ThreadLocal<Collection<V>>() {
		@Override
		protected Collection<V> initialValue() {
			return new ArrayList<>();
		}
	};
	private final ThreadLocal<Set<Entry<K, V>>> entrySet = new ThreadLocal<Set<Entry<K, V>>>() {
		@Override
		protected Set<Entry<K, V>> initialValue() {
			return new HashSet<>();
		}
	};
	private final ThreadLocal<ToSetConsumer<K>> keyConsumer = new ThreadLocal<ToSetConsumer<K>>() {
		@Override
		protected ToSetConsumer<K> initialValue() {
			return new ToSetConsumer<K>();
		}
	};
	private final ThreadLocal<ToCollectionConsumer<V>> valueConsumer = new ThreadLocal<ToCollectionConsumer<V>>() {
		@Override
		protected ToCollectionConsumer<V> initialValue() {
			return new ToCollectionConsumer<V>();
		}
	};
	private final ThreadLocal<ToSetConsumer<Entry<K, V>>> entryConsumer = new ThreadLocal<ToSetConsumer<Entry<K, V>>>() {
		@Override
		protected ToSetConsumer<Entry<K, V>> initialValue() {
			return new ToSetConsumer<Entry<K, V>>();
		}
	};

	@Override
	public int size() {
		lock.lockRead();
		final int result = internalMap.size();
		lock.unlockRead();
		return result;
	}

	@Override
	public boolean isEmpty() {
		return size() == 0;
	}

	@Override
	public boolean containsKey(Object key) {
		lock.lockRead();
		final boolean result = internalMap.containsKey(key);
		lock.unlockRead();
		return result;
	}

	@Override
	public boolean containsValue(Object value) {
		lock.lockRead();
		final boolean result = internalMap.containsValue(value);
		lock.unlockRead();
		return result;
	}

	@Override
	public V get(Object key) {
		lock.lockRead();
		final V result = internalMap.get(key);
		lock.unlockRead();
		return result;
	}

	@Override
	public V put(K key, V value) {
		lock.lockWrite();
		final V result = internalMap.put(key, value);
		lock.unlockWrite();
		return result;
	}

	@Override
	public V remove(Object key) {
		lock.lockWrite();
		final V result = internalMap.remove(key);
		lock.unlockWrite();
		return result;
	}

	@Override
	public void putAll(Map<? extends K, ? extends V> m) {
		lock.lockWrite();
		internalMap.putAll(m);
		lock.unlockWrite();
	}

	@Override
	public void clear() {
		lock.lockWrite();
		internalMap.clear();
		lock.unlockWrite();
	}

	@Override
	public Set<K> keySet() {
		lock.lockRead();
		final Set<K> result = keySet.get();
		try {
			result.clear();
			//addAll allocates arrays unnecessarily
			final ToSetConsumer<K> consumer = keyConsumer.get();
			consumer.setSet(result);
			internalMap.keySet().forEach(consumer);
		} finally {
			lock.unlockRead();
		}
		return result;
	}

	@Override
	public Collection<V> values() {
		lock.lockRead();
		final Collection<V> result = values.get();
		try {
			result.clear();
			//addAll allocates arrays unnecessarily
			final ToCollectionConsumer<V> consumer = valueConsumer.get();
			consumer.setCollection(result);
			internalMap.values().forEach(consumer);
		} finally {
			lock.unlockRead();
		}
		return result;
	}

	@Override
	public Set<Entry<K, V>> entrySet() {
		lock.lockRead();
		final Set<Entry<K, V>> result = entrySet.get();
		try {
			result.clear();
			//addAll allocates arrays unnecessarily
			final ToSetConsumer<Entry<K, V>> consumer = entryConsumer.get();
			consumer.setSet(result);
			internalMap.entrySet().forEach(consumer);
		} finally {
			lock.unlockRead();
		}
		return result;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/threadpool/DefaultThreadPoolProvider.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core.threadpool;

import org.mini2Dx.miniscript.core.ThreadPoolProvider;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class DefaultThreadPoolProvider implements ThreadPoolProvider {
	private static final String THREAD_NAME_PREFIX = "miniscript-thread-";
	private static final AtomicInteger THREAD_ID = new AtomicInteger(0);

	private final ScheduledExecutorService executorService;

	public DefaultThreadPoolProvider() {
		this(Runtime.getRuntime().availableProcessors() + 1);
	}

	public DefaultThreadPoolProvider(int maxConcurrentScripts) {
		executorService = Executors.newScheduledThreadPool(
				Math.min(maxConcurrentScripts + 1, Runtime.getRuntime().availableProcessors() * 2),
				new ThreadFactory() {
					@Override
					public Thread newThread(Runnable r) {
						return new Thread(r, THREAD_NAME_PREFIX + THREAD_ID.getAndIncrement());
					}
				});
	}

	@Override
	public Future<?> submit(Runnable task) {
		return executorService.submit(task);
	}

	@Override
	public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
		return executorService.scheduleAtFixedRate(command, initialDelay, period, unit);
	}

	@Override
	public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
		return executorService.schedule(command, delay, unit);
	}

	@Override
	public void shutdown(boolean interruptThreads) {
		if(interruptThreads) {
			executorService.shutdownNow();
		} else {
			executorService.shutdown();
		}
	}
}


<|start_filename|>gradle-plugin/src/main/java/org/mini2Dx/miniscript/gradle/CompilerInputFile.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.gradle;

import java.io.File;

public class CompilerInputFile {
	private final File inputScriptFile;
	private final String inputScriptFilename;
	private final String inputScriptFileSuffix;
	private final String inputScriptFilenameWithoutSuffix;
	private final String inputScriptRelativeFilename;
	private final String outputClassName;

	public CompilerInputFile(File scriptsRootDir, File scriptFile) {
		super();
		this.inputScriptFile = scriptFile;
		this.inputScriptFilename = inputScriptFile.getName();
		this.inputScriptFileSuffix = inputScriptFilename.substring(inputScriptFilename.lastIndexOf('.')).toLowerCase();
		this.inputScriptFilenameWithoutSuffix = inputScriptFile.getName().substring( 0, inputScriptFile.getName().lastIndexOf('.'));
		final String inputScriptRelativeFilename = inputScriptFile.getAbsolutePath().replace(scriptsRootDir.getAbsolutePath(), "");

		if(inputScriptRelativeFilename.startsWith("/") || inputScriptRelativeFilename.startsWith("\\")) {
			this.inputScriptRelativeFilename  = inputScriptRelativeFilename.substring(1);
		} else {
			this.inputScriptRelativeFilename = inputScriptRelativeFilename;
		}

		this.outputClassName = inputScriptFilenameWithoutSuffix.replace('-', '_');
	}

	public File getInputScriptFile() {
		return inputScriptFile;
	}

	public String getInputScriptFilename() {
		return inputScriptFilename;
	}

	public String getInputScriptFileSuffix() {
		return inputScriptFileSuffix;
	}

	public String getInputScriptFilenameWithoutSuffix() {
		return inputScriptFilenameWithoutSuffix;
	}

	public String getInputScriptRelativeFilename() {
		return inputScriptRelativeFilename;
	}

	public String getOutputClassName() {
		return outputClassName;
	}

	public String getOutputPackageName(String rootScriptPackage) {
		if(inputScriptRelativeFilename.indexOf('/') > 0 || inputScriptRelativeFilename.indexOf('\\') > 0) {
			final String initialResult = rootScriptPackage + '.' + inputScriptRelativeFilename.
					substring(0, inputScriptRelativeFilename.indexOf('/') > 0 ?
							inputScriptRelativeFilename.lastIndexOf('/') :
							inputScriptRelativeFilename.lastIndexOf('\\')).
					replace('/', '.').
					replace('\\', '.').
					replace('-', '_');

			final StringBuilder result = new StringBuilder();
			result.append(initialResult.charAt(0));
			for(int i = 1; i < initialResult.length(); i++) {
				if(initialResult.charAt(i - 1) != '.') {
					result.append(initialResult.charAt(i));
					continue;
				}
				if(Character.isJavaIdentifierStart(initialResult.charAt(i))) {
					result.append(initialResult.charAt(i));
					continue;
				}
				result.append('_');
				result.append(initialResult.charAt(i));
			}
			return result.toString();
		}
		return rootScriptPackage;
	}
}


<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/dummy/DummyScriptExecutorPool.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core.dummy;

import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.mini2Dx.miniscript.core.GameScript;
import org.mini2Dx.miniscript.core.GameScriptingEngine;
import org.mini2Dx.miniscript.core.ScriptBindings;
import org.mini2Dx.miniscript.core.ScriptExecutionTask;
import org.mini2Dx.miniscript.core.ScriptExecutor;
import org.mini2Dx.miniscript.core.ScriptExecutorPool;
import org.mini2Dx.miniscript.core.ScriptInvocationListener;
import org.mini2Dx.miniscript.core.exception.InsufficientCompilersException;
import org.mini2Dx.miniscript.core.exception.ScriptExecutorUnavailableException;

/**
 * An implementation for {@link ScriptExecutorPool} for unit tests
 */
public class DummyScriptExecutorPool implements ScriptExecutorPool<DummyScript> {
	private final Map<Integer, GameScript<DummyScript>> scripts = new ConcurrentHashMap<Integer, GameScript<DummyScript>>();
	private final Map<String, Integer> filepathsToScriptIds = new ConcurrentHashMap<String, Integer>();
	private final Map<Integer, String> scriptIdsToFilepaths = new ConcurrentHashMap<Integer, String>();
	private final BlockingQueue<ScriptExecutor<DummyScript>> executors;
	private final GameScriptingEngine gameScriptingEngine;

	public DummyScriptExecutorPool(GameScriptingEngine gameScriptingEngine, int poolSize) {
		this.gameScriptingEngine = gameScriptingEngine;
		executors = new ArrayBlockingQueue<ScriptExecutor<DummyScript>>(poolSize);

		for (int i = 0; i < poolSize; i++) {
			executors.offer(new DummyScriptExecutor(this));
		}
	}

	@Override
	public int preCompileScript(String filepath, String scriptContent) throws InsufficientCompilersException {
		ScriptExecutor<DummyScript> executor = executors.poll();
		if (executor == null) {
			throw new InsufficientCompilersException();
		}
		GameScript<DummyScript> script = executor.compile(scriptContent);
		executor.release();
		scripts.put(script.getId(), script);
		filepathsToScriptIds.put(filepath, script.getId());
		scriptIdsToFilepaths.put(script.getId(), filepath);
		return script.getId();
	}

	@Override
	public int getCompiledScriptId(String filepath) {
		return filepathsToScriptIds.get(filepath);
	}

	@Override
	public String getCompiledScriptPath(int scriptId) {
		return scriptIdsToFilepaths.get(scriptId);
	}

	@Override
	public ScriptExecutionTask<?> execute(int taskId, int scriptId, ScriptBindings scriptBindings,
			ScriptInvocationListener invocationListener, boolean syncCall) {
		ScriptExecutor<DummyScript> executor = allocateExecutor();
		if (executor == null) {
			throw new ScriptExecutorUnavailableException(scriptId);
		}
		return new ScriptExecutionTask<DummyScript>(taskId, gameScriptingEngine, executor, scriptId,
				scripts.get(scriptId), scriptBindings, invocationListener, syncCall);
	}

	@Override
	public void release(ScriptExecutor<DummyScript> executor) {
		try {
			executors.put(executor);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private ScriptExecutor<DummyScript> allocateExecutor() {
		try {
			return executors.take();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public GameScriptingEngine getGameScriptingEngine() {
		return gameScriptingEngine;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/GameFuture.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import org.mini2Dx.miniscript.core.exception.ScriptSkippedException;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Represents a task that will complete in-game at a future time
 */
public abstract class GameFuture {
	private static final AtomicInteger ID_GENERATOR = new AtomicInteger(0);

	private static final int STATE_NONE = 0;
	private static final int STATE_FUTURE_SKIPPED = 1;
	private static final int STATE_FUTURE_SKIPPED_GC_READY = STATE_FUTURE_SKIPPED * 10;
	private static final int STATE_SCRIPT_SKIPPED = 2;
	private static final int STATE_SCRIPT_SKIPPED_GC_READY = STATE_SCRIPT_SKIPPED * 10;
	private static final int STATE_COMPLETED = 3;
	private static final int STATE_COMPLETED_GC_READY = STATE_COMPLETED * 10;

	private final int futureId;
	private final AtomicInteger state = new AtomicInteger(STATE_NONE);

	/**
	 * Constructor using {@link GameScriptingEngine#MOST_RECENT_INSTANCE}
	 */
	public GameFuture() {
		this(GameScriptingEngine.MOST_RECENT_INSTANCE);
	}

	/**
	 * Constructor
	 *
	 * @param gameScriptingEngine
	 *            The {@link GameScriptingEngine} this future belongs to
	 */
	public GameFuture(GameScriptingEngine gameScriptingEngine) {
		if(gameScriptingEngine == null) {
			throw new RuntimeException("Cannot pass null scripting engine to " + GameFuture.class.getSimpleName());
		}
		futureId = ID_GENERATOR.incrementAndGet();
		gameScriptingEngine.submitGameFuture(this);

		if (Thread.interrupted()) {
			state.compareAndSet(STATE_NONE, STATE_SCRIPT_SKIPPED);
			throw new ScriptSkippedException();
		}
	}

	/**
	 * Update the {@link GameFuture}
	 *
	 * @param delta
	 *            The amount of time (in seconds) since the last frame
	 * @return True if the {@link GameFuture} has completed
	 */
	protected abstract boolean update(float delta);

	/**
	 * Called when the {@link GameFuture} is skipped
	 */
	protected abstract void onFutureSkipped();

	/**
	 * Called when the entire script is skipped
	 */
	protected abstract void onScriptSkipped();

	void evaluate(float delta) {
		if(isReadyForGC()) {
			return;
		}
		if(isScriptSkipped()) {
			onScriptSkipped();
			state.compareAndSet(STATE_SCRIPT_SKIPPED, STATE_SCRIPT_SKIPPED_GC_READY);
			return;
		}
		if(isFutureSkipped()) {
			onFutureSkipped();
			state.compareAndSet(STATE_FUTURE_SKIPPED, STATE_FUTURE_SKIPPED_GC_READY);
			return;
		}
		if (update(delta)) {
			complete();
			state.compareAndSet(STATE_COMPLETED, STATE_COMPLETED_GC_READY);
		}
	}

	protected void complete() {
		if(!state.compareAndSet(STATE_NONE, STATE_COMPLETED)) {
			return;
		}
		synchronized (this) {
			notifyAll();
		}
	}

	public void skipFuture() {
		state.compareAndSet(STATE_NONE, STATE_FUTURE_SKIPPED);
		synchronized (this) {
			notifyAll();
		}
	}

	/**
	 * Can be called in a script to wait for this {@link GameFuture} to complete
	 *
	 * @throws ScriptSkippedException
	 *             Thrown when the script is skipped
	 */
	public void waitForCompletion() throws ScriptSkippedException {
		while (!isCompleted()) {
			if (isFutureSkipped()) {
				return;
			}
			if (isScriptSkipped()) {
				throw new ScriptSkippedException();
			}
			if (Thread.interrupted()) {
				state.compareAndSet(STATE_NONE, STATE_SCRIPT_SKIPPED);
				throw new ScriptSkippedException();
			}
			try {
				synchronized (this) {
					wait();
				}
			} catch (InterruptedException e) {
				state.compareAndSet(STATE_NONE, STATE_SCRIPT_SKIPPED);
				throw new ScriptSkippedException();
			}
		}
	}

	/**
	 * Alias to {@link #waitForCompletion()}
	 * @throws ScriptSkippedException
	 */
	public void wfc() throws ScriptSkippedException {
		waitForCompletion();
	}

	/**
	 * Returns if this {@link GameFuture} is complete
	 *
	 * @return True if this completed without being skipped
	 */
	public boolean isCompleted() {
		switch (state.get()) {
		case STATE_COMPLETED:
		case STATE_COMPLETED_GC_READY:
			return true;
		}
		return false;
	}

	/**
	 * Returns if this {@link GameFuture} was skipped
	 *
	 * @return True if this {@link GameFuture} was skipped
	 */
	public boolean isFutureSkipped() {
		switch (state.get()) {
		case STATE_FUTURE_SKIPPED:
		case STATE_FUTURE_SKIPPED_GC_READY:
			return true;
		}
		return false;
	}

	/**
	 * Returns if the script was skipped
	 *
	 * @return True if this {@link GameFuture} was waiting for completion when
	 *         the script was skipped
	 */
	public boolean isScriptSkipped() {
		switch (state.get()) {
		case STATE_SCRIPT_SKIPPED:
		case STATE_SCRIPT_SKIPPED_GC_READY:
			return true;
		}
		return false;
	}

	/**
	 * Returns if the {@link GameFuture} is ready for cleanup
	 * @return True if ready for cleanup
	 */
	public boolean isReadyForGC() {
		switch (state.get()) {
		case STATE_COMPLETED_GC_READY:
		case STATE_FUTURE_SKIPPED_GC_READY:
		case STATE_SCRIPT_SKIPPED_GC_READY:
			return true;
		}
		return false;
	}

	/**
	 * Returns this unique id of this {@link GameFuture} instance
	 * @return
	 */
	public int getFutureId() {
		return futureId;
	}
}


<|start_filename|>lua/src/test/java/org/mini2Dx/miniscript/lua/LuaScriptExecutorPoolTest.java<|end_filename|>
package org.mini2Dx.miniscript.lua;

import org.junit.Assert;
import org.junit.Test;
import org.luaj.vm2.lib.jse.JsePlatform;
import org.mini2Dx.miniscript.core.*;
import org.mini2Dx.miniscript.core.dummy.DummyGameScriptingEngine;
import org.mini2Dx.miniscript.lua.LuaScriptExecutorPool;

import java.util.HashMap;
import java.util.Map;

/**
 * Copyright 2019 Viridian Software Ltd.
 */

public class LuaScriptExecutorPoolTest implements ScriptInvocationListener {

	@Test
	public void testScriptIds() {

		DummyGameScriptingEngine dummyGameScriptingEngine = new DummyGameScriptingEngine();
		GeneratedClasspathScriptProvider classpathScriptProvider = new GeneratedClasspathScriptProvider() {
			@Override
			public Map getGeneratedScripts() {
				final HashMap results = new HashMap();
				results.put("script1", JsePlatform.standardGlobals().load("print(\"script1\")"));
				results.put("script2", JsePlatform.standardGlobals().load("print(\"script2\")"));
				results.put("script3", JsePlatform.standardGlobals().load("print(\"script3\")"));
				results.put("script4", JsePlatform.standardGlobals().load("print(\"script4\")"));
				return results;
			}
		};

		LuaScriptExecutorPool luaScriptExecutorPool = new LuaScriptExecutorPool(dummyGameScriptingEngine, classpathScriptProvider, 4, false);

		ScriptBindings scriptBindings = new ScriptBindings();

		ScriptExecutionTask scriptExecutionTask = luaScriptExecutorPool.execute(0,0, scriptBindings, this, true);
		Assert.assertEquals(0, scriptExecutionTask.getScriptId());
		scriptExecutionTask = luaScriptExecutorPool.execute(1, 1, scriptBindings, this, true);
		Assert.assertEquals(1, scriptExecutionTask.getScriptId());
		scriptExecutionTask = luaScriptExecutorPool.execute(2, 2, scriptBindings, this, true);
		Assert.assertEquals(2, scriptExecutionTask.getScriptId());
		scriptExecutionTask = luaScriptExecutorPool.execute(2, 3, scriptBindings, this, true);
		Assert.assertEquals(3, scriptExecutionTask.getScriptId());
	}

	@Override
	public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
	}

	@Override
	public void onScriptSkipped(int scriptId) {
	}

	@Override
	public void onScriptException(int scriptId, Exception e) {
	}

	@Override
	public boolean callOnGameThread() {
		return false;
	}
}


<|start_filename|>groovy/src/main/java/org/mini2Dx/miniscript/groovy/GroovyGameScriptingEngine.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.groovy;

import org.mini2Dx.miniscript.core.ClasspathScriptProvider;
import org.mini2Dx.miniscript.core.GameScriptingEngine;
import org.mini2Dx.miniscript.core.ScriptExecutorPool;
import org.mini2Dx.miniscript.core.exception.SandboxingUnsupportedException;

/**
 * An implementation of {@link GameScriptingEngine} for Groovy-based scripts
 */
public class GroovyGameScriptingEngine extends GameScriptingEngine {
	private static final String SCRIPTING_LANGUAGE = "Groovy";
	
	/**
	 * Constructs a scripting engine backed by a thread pool with the maximum
	 * amount of concurrent scripts set to the amount of processors + 1;
	 */
	public GroovyGameScriptingEngine() {
		super();
	}

	/**
	 * Constructs a scripting engine backed by a thread pool.
	 * 
	 * @param maxConcurrentScripts
	 *            The maximum amount of concurrently running scripts. Note this
	 *            is a 'requested' amount and may be less due to the amount of
	 *            available processors on the player's machine.
	 */
	public GroovyGameScriptingEngine(int maxConcurrentScripts) {
		super(maxConcurrentScripts);
	}

	@Override
	protected ScriptExecutorPool<?> createScriptExecutorPool(ClasspathScriptProvider classpathScriptProvider, int poolSize, boolean sandboxing) {
		if(sandboxing) {
			throw new SandboxingUnsupportedException(SCRIPTING_LANGUAGE);
		}
		return new GroovyScriptExecutorPool(this, poolSize);
	}

	@Override
	public boolean isSandboxingSupported() {
		return false;
	}

	@Override
	public boolean isEmbeddedSynchronousScriptSupported() {
		return true;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/util/AbstractConcurrentQueue.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core.util;

import org.mini2Dx.lockprovider.ReadWriteLock;
import org.mini2Dx.miniscript.core.GameScriptingEngine;

import java.util.Collection;
import java.util.Iterator;
import java.util.Queue;

public abstract class AbstractConcurrentQueue<E> implements Queue<E> {
	protected final ReadWriteLock lock = GameScriptingEngine.LOCK_PROVIDER.newReadWriteLock();

	protected final Queue<E> internalQueue;

	public AbstractConcurrentQueue(Queue<E> internalQueue) {
		this.internalQueue = internalQueue;
	}

	@Override
	public int size() {
		lock.lockRead();
		final int result = internalQueue.size();
		lock.unlockRead();
		return result;
	}

	@Override
	public boolean isEmpty() {
		return size() == 0;
	}

	@Override
	public boolean contains(Object o) {
		lock.lockRead();
		final boolean result = internalQueue.contains(o);
		lock.unlockRead();
		return result;
	}

	@Override
	public Iterator<E> iterator() {
		throw new UnsupportedOperationException();
	}

	@Override
	public Object[] toArray() {
		lock.lockRead();
		try {
			return internalQueue.toArray();
		} finally {
			lock.unlockRead();
		}
	}

	@Override
	public <T> T[] toArray(T[] a) {
		lock.lockRead();
		try {
			return internalQueue.toArray(a);
		} finally {
			lock.unlockRead();
		}
	}

	@Override
	public boolean add(E e) {
		lock.lockWrite();
		final boolean result = internalQueue.add(e);
		lock.unlockWrite();
		return result;
	}

	@Override
	public boolean remove(Object o) {
		lock.lockWrite();
		final boolean result = internalQueue.remove(o);
		lock.unlockWrite();
		return result;
	}

	@Override
	public boolean containsAll(Collection<?> c) {
		lock.lockRead();
		final boolean result = internalQueue.containsAll(c);
		lock.unlockRead();
		return result;
	}

	@Override
	public boolean addAll(Collection<? extends E> c) {
		lock.lockWrite();
		final boolean result = internalQueue.addAll(c);
		lock.unlockWrite();
		return result;
	}

	@Override
	public boolean removeAll(Collection<?> c) {
		lock.lockWrite();
		final boolean result = internalQueue.removeAll(c);
		lock.unlockWrite();
		return result;
	}

	@Override
	public boolean retainAll(Collection<?> c) {
		lock.lockWrite();
		final boolean result = internalQueue.retainAll(c);
		lock.unlockWrite();
		return result;
	}

	@Override
	public void clear() {
		lock.lockWrite();
		internalQueue.clear();
		lock.unlockWrite();
	}

	@Override
	public boolean offer(E e) {
		return add(e);
	}

	public boolean lazyOffer(E e) {
		while(!lock.tryLockWrite()) {
			try {
				Thread.sleep(0, 100);
			} catch (Exception ex) {}
		}
		final boolean result = internalQueue.add(e);
		lock.unlockWrite();
		return result;
	}

	@Override
	public E remove() {
		lock.lockWrite();
		try {
			return internalQueue.remove();
		} finally {
			lock.unlockWrite();
		}
	}

	@Override
	public E poll() {
		lock.lockRead();
		if(internalQueue.isEmpty()) {
			lock.unlockRead();
			return null;
		}
		lock.unlockRead();

		lock.lockWrite();
		try {
			return internalQueue.poll();
		} finally {
			lock.unlockWrite();
		}
	}

	@Override
	public E element() {
		lock.lockRead();
		try {
			return internalQueue.element();
		} finally {
			lock.unlockRead();
		}
	}

	@Override
	public E peek() {
		lock.lockRead();
		if(internalQueue.isEmpty()) {
			lock.unlockRead();
			return null;
		}
		lock.unlockRead();

		lock.lockRead();
		try {
			return internalQueue.peek();
		} finally {
			lock.unlockRead();
		}
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/ThreadPoolProvider.java<|end_filename|>
/**
 * Copyright 2019 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core;

import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

public interface ThreadPoolProvider {

	Future<?> submit(Runnable task);

	ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);

	public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);

	void shutdown(boolean interruptThreads);

}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/util/ReadWriteIntMap.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import org.mini2Dx.lockprovider.ReadWriteLock;
import org.mini2Dx.miniscript.core.GameScriptingEngine;

public class ReadWriteIntMap<T> extends IntMap<T> {
	protected ReadWriteLock lock = GameScriptingEngine.LOCK_PROVIDER.newReadWriteLock();

	private final ThreadLocal<Keys> keys = new ThreadLocal<Keys>() {
		@Override
		protected Keys initialValue() {
			return new Keys(ReadWriteIntMap.this);
		}
	};
	private final ThreadLocal<Values> values = new ThreadLocal<Values>() {
		@Override
		protected Values initialValue() {
			return new Values(ReadWriteIntMap.this);
		}
	};
	private final ThreadLocal<Entries> entries = new ThreadLocal<Entries>() {
		@Override
		protected Entries initialValue() {
			return new Entries(ReadWriteIntMap.this);
		}
	};

	/**
	 * Creates a new map with an initial capacity of 51 and a load factor of 0.8.
	 */
	public ReadWriteIntMap() {
		super();
	}

	/**
	 * Creates a new map with a load factor of 0.8.
	 *
	 * @param initialCapacity If not a power of two, it is increased to the next nearest power of two.
	 */
	public ReadWriteIntMap(int initialCapacity) {
		super(initialCapacity);
	}

	/**
	 * Creates a new map with the specified initial capacity and load factor. This map will hold initialCapacity items before
	 * growing the backing table.
	 *
	 * @param initialCapacity If not a power of two, it is increased to the next nearest power of two.
	 * @param loadFactor
	 */
	public ReadWriteIntMap(int initialCapacity, float loadFactor) {
		super(initialCapacity, loadFactor);
	}

	/**
	 * Creates a new map identical to the specified map.
	 *
	 * NOTE: read access to the other map is not thread-safe
	 *
	 * @param map
	 */
	public ReadWriteIntMap(IntMap<? extends T> map) {
		super(map);
	}

	/**
	 * Returns the size in a thread-safe manner
	 * @return 0 if empty
	 */
	public int size() {
		lock.lockRead();
		final int result = super.size;
		lock.unlockRead();
		return result;
	}

	@Override
	public T put(int key, T value) {
		lock.lockWrite();
		T t = super.put(key, value);
		lock.unlockWrite();
		return t;
	}

	/**
	 * Puts a key/value if the key is not already present
	 * @param key The key to put if absent
	 * @param value The value to put if absent
	 * @return True if the value was put
	 */
	public boolean putIfAbsent(int key, T value) {
		boolean result = false;
		lock.lockWrite();
		if(!super.containsKey(key)) {
			super.put(key, value);
			result = true;
		}
		lock.unlockWrite();
		return result;
	}

	/**
	 * Puts a key/value if the key is already present
	 * @param key The key to put if present
	 * @param value The value to put if present
	 * @return True if the value was put
	 */
	public boolean putIfPresent(int key, T value) {
		boolean result = false;
		lock.lockWrite();
		if(super.containsKey(key)) {
			super.put(key, value);
			result = true;
		}
		lock.unlockWrite();
		return result;
	}

	@Override
	public void putAll(IntMap<? extends  T> map) {
		boolean isOtherConcurrent = map instanceof ReadWriteIntMap;
		if (isOtherConcurrent){
			((ReadWriteIntMap) map).getLock().lockRead();
		}
		lock.lockWrite();
		super.putAll(map);
		lock.unlockWrite();
		if (isOtherConcurrent){
			((ReadWriteIntMap) map).getLock().unlockRead();
		}
	}

	/**
	 * @param key
	 * @param defaultValue Returned if the key was not associated with a value.
	 */
	@Override
	public T get(int key, T defaultValue) {
		lock.lockRead();
		T t = super.get(key, defaultValue);
		lock.unlockRead();
		return t;
	}

	@Override
	public T get(int key) {
		lock.lockRead();
		T t = super.get(key);
		lock.unlockRead();
		return t;
	}

	@Override
	public T remove(int key) {
		lock.lockWrite();
		T t = super.remove(key);
		lock.unlockWrite();
		return t;
	}

	/**
	 * Returns true if the map has one or more items.
	 */
	@Override
	public boolean notEmpty() {
		lock.lockRead();
		boolean b = super.notEmpty();
		lock.unlockRead();
		return b;
	}

	/**
	 * Returns true if the map is empty.
	 */
	@Override
	public boolean isEmpty() {
		lock.lockRead();
		boolean b = super.isEmpty();
		lock.unlockRead();
		return b;
	}

	/**
	 * Reduces the size of the backing arrays to be the specified capacity or less. If the capacity is already less, nothing is
	 * done. If the map contains more items than the specified capacity, the next highest power of two capacity is used instead.
	 *
	 * @param maximumCapacity
	 */
	@Override
	public void shrink(int maximumCapacity) {
		lock.lockWrite();
		super.shrink(maximumCapacity);
		lock.unlockWrite();
	}

	/**
	 * Clears the map and reduces the size of the backing arrays to be the specified capacity if they are larger.
	 *
	 * @param maximumCapacity
	 */
	@Override
	public void clear(int maximumCapacity) {
		lock.lockWrite();
		super.clear(maximumCapacity);
		lock.unlockWrite();
	}

	@Override
	public void clear() {
		lock.lockWrite();
		super.clear();
		lock.unlockWrite();
	}

	@Override
	public boolean containsKey(int key) {
		lock.lockRead();
		boolean b = super.containsKey(key);
		lock.unlockRead();
		return b;
	}

	/**
	 * Returns the key for the specified value, or <tt>notFound</tt> if it is not in the map. Note this traverses the entire map
	 * and compares every value, which may be an expensive operation.
	 *
	 * @param value
	 * @param identity If true, uses == to compare the specified value with values in the map. If false, uses
	 *                 {@link #equals(Object)}.
	 * @param notFound
	 */
	@Override
	public int findKey(Object value, boolean identity, int notFound) {
		lock.lockRead();
		int i = super.findKey(value, identity, notFound);
		lock.unlockRead();
		return i;
	}

	/**
	 * Increases the size of the backing array to accommodate the specified number of additional items. Useful before adding many
	 * items to avoid multiple backing array resizes.
	 *
	 * @param additionalCapacity
	 */
	@Override
	public void ensureCapacity(int additionalCapacity) {
		lock.lockWrite();
		super.ensureCapacity(additionalCapacity);
		lock.unlockWrite();
	}

	@Override
	public int hashCode() {
		lock.lockRead();
		int i = super.hashCode();
		lock.unlockRead();
		return i;
	}

	@Override
	public boolean equals(Object obj) {
		boolean isOtherConcurrent = obj instanceof ReadWriteIntMap;
		if (isOtherConcurrent){
			((ReadWriteIntMap) obj).getLock().lockRead();
		}
		lock.lockRead();
		boolean b = super.equals(obj);
		lock.unlockRead();
		if (isOtherConcurrent){
			((ReadWriteIntMap) obj).getLock().unlockRead();
		}
		return b;
	}

	@Override
	public String toString() {
		lock.lockRead();
		String s = super.toString();
		lock.unlockRead();
		return s;
	}

	@Override
	public Entries<T> entries() {
		final Entries<T> entries = this.entries.get();
		lock.lockRead();
		entries.reset();
		lock.unlockRead();
		return entries;
	}

	@Override
	public Values<T> values() {
		final Values<T> values = this.values.get();
		lock.lockRead();
		values.reset();
		lock.unlockRead();
		return values;
	}

	@Override
	public Keys keys() {
		final Keys keys = this.keys.get();
		lock.lockRead();
		keys.reset();
		lock.unlockRead();
		return keys;
	}

	public ReadWriteLock getLock() {
		return lock;
	}
}

<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/ScriptInvocation.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

/**
 * Wraps a script invocation so it can be queued concurrently
 */
public class ScriptInvocation implements Comparable<ScriptInvocation> {
	private final ScriptInvocationPool invocationPool;

	private int taskId;
	private int scriptId;
	private ScriptBindings scriptBindings;
	private ScriptInvocationListener invocationListener;
	private int priority;
	private long invokeTimestamp;
	private boolean interactive;
	
	ScriptInvocation(ScriptInvocationPool invocationPool) {
		this.invocationPool = invocationPool;
	}

	public int getScriptId() {
		return scriptId;
	}
	
	public void setScriptId(int scriptId) {
		this.scriptId = scriptId;
	}
	
	public ScriptBindings getScriptBindings() {
		return scriptBindings;
	}

	public void setScriptBindings(ScriptBindings scriptBindings) {
		this.scriptBindings = scriptBindings;
	}

	public ScriptInvocationListener getInvocationListener() {
		return invocationListener;
	}
	
	public void setInvocationListener(ScriptInvocationListener invocationListener) {
		this.invocationListener = invocationListener;
	}

	public int getPriority() {
		return priority;
	}

	public void setPriority(int priority) {
		this.priority = priority;
	}

	public boolean isInteractive() {
		return interactive;
	}

	public void setInteractive(boolean interactive) {
		this.interactive = interactive;
	}

	public int getTaskId() {
		return taskId;
	}

	public void setTaskId(int taskId) {
		this.taskId = taskId;
	}

	public long getInvokeTimestamp() {
		return invokeTimestamp;
	}

	public void setInvokeTimestamp(long invokeTimestamp) {
		this.invokeTimestamp = invokeTimestamp;
	}

	public void release() {
		taskId = 0;
		priority = 0;
		invokeTimestamp = 0L;
		interactive = false;
		invocationListener = null;
		invocationPool.release(this);
	}

	@Override
	public int compareTo(ScriptInvocation o) {
		final int priorityCompare = Integer.compare(o.priority, priority);
		if(priorityCompare == 0) {
			return Long.compare(invokeTimestamp, o.invokeTimestamp);
		}
		return priorityCompare;
	}
}


<|start_filename|>kotlin/src/test/resources/invokeWithinScript.kts<|end_filename|>
val scripts = bindings["scripts"] as org.mini2Dx.miniscript.core.EmbeddedScriptInvoker
scripts.invokeSync("default.kts")
val intValue2: Int = 102

<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/ScriptExecutionTask.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import org.mini2Dx.miniscript.core.exception.ScriptSkippedException;
import org.mini2Dx.miniscript.core.notification.ScriptBeginNotification;
import org.mini2Dx.miniscript.core.notification.ScriptExceptionNotification;
import org.mini2Dx.miniscript.core.notification.ScriptSkippedNotification;
import org.mini2Dx.miniscript.core.notification.ScriptSuccessNotification;

import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Executes a script
 */
public class ScriptExecutionTask<S> implements Runnable {
	private final int taskId;
	private final int scriptId;
	private final GameScriptingEngine scriptingEngine;
	private final ScriptExecutor<S> executor;
	private final GameScript<S> script;
	private final ScriptBindings scriptBindings;
	private final ScriptInvocationListener scriptInvocationListener;
	private final boolean syncCall;

	private final AtomicBoolean completed = new AtomicBoolean(false);
	private Future<?> taskFuture;

	public ScriptExecutionTask(int taskId, GameScriptingEngine gameScriptingEngine, ScriptExecutor<S> executor,
							   int scriptId, GameScript<S> script, ScriptBindings scriptBindings,
							   ScriptInvocationListener scriptInvocationListener, boolean syncCall) {
		this.taskId = taskId;
		this.scriptingEngine = gameScriptingEngine;
		this.executor = executor;
		this.scriptId = scriptId;
		this.script = script;
		this.scriptBindings = scriptBindings;
		this.scriptInvocationListener = scriptInvocationListener;
		this.syncCall = syncCall;
	}

	@Override
	public void run() {
		try {
			if(scriptInvocationListener != null) {
				if(scriptInvocationListener instanceof InteractiveScriptListener) {
					final ScriptInvocationListener actualListener = ((InteractiveScriptListener) scriptInvocationListener).getInvocationListener();
					invokeOnScriptBegin(actualListener);
				} else {
					invokeOnScriptBegin(scriptInvocationListener);
				}
			}

			ScriptExecutionResult executionResult = executor.execute(scriptId, script, scriptBindings,
					scriptInvocationListener != null);
			if (scriptInvocationListener != null) {
				if (scriptInvocationListener.callOnGameThread()) {
					scriptingEngine.scriptNotifications.offer(
							new ScriptSuccessNotification(scriptInvocationListener, script.getId(), executionResult));
				} else {
					scriptInvocationListener.onScriptSuccess(script.getId(), executionResult);
				}
			}
		} catch (InterruptedException | ScriptSkippedException e) {
			if (scriptInvocationListener != null) {
				if (scriptInvocationListener.callOnGameThread()) {
					scriptingEngine.scriptNotifications
							.offer(new ScriptSkippedNotification(scriptInvocationListener, script.getId()));
				} else {
					scriptInvocationListener.onScriptSkipped(script.getId());
				}
			}
		} catch (Exception e) {
			if (scriptInvocationListener != null) {
				if (scriptInvocationListener.callOnGameThread()) {
					scriptingEngine.scriptNotifications
							.offer(new ScriptExceptionNotification(scriptInvocationListener, script.getId(), e));
				} else {
					scriptInvocationListener.onScriptException(script.getId(), e);
				}
			} else {
				e.printStackTrace();
			}
		}
		// Clear interrupted bit
		Thread.interrupted();
		completed.set(true);
	}

	private void invokeOnScriptBegin(ScriptInvocationListener listener) throws InterruptedException {
		if(listener == null) {
			return;
		}
		if(listener.callOnGameThread() && !syncCall) {
			final ScriptBeginNotification beginNotification = new ScriptBeginNotification(listener, scriptId);
			scriptingEngine.scriptNotifications.offer(beginNotification);

			while(!beginNotification.isProcessed()) {
				synchronized(beginNotification) {
					beginNotification.wait();
				}
			}
		} else {
			listener.onScriptBegin(scriptId);
		}
	}

	public void skipScript() {
		if (taskFuture.isDone()) {
			return;
		}
		if (!taskFuture.cancel(true)) {

		}
	}

	public boolean isFinished() {
		return completed.get();
	}

	public void cleanup() {
		executor.release();
	}

	public int getTaskId() {
		return taskId;
	}

	public int getScriptId() {
		return script.getId();
	}

	public void setTaskFuture(Future<?> taskFuture) {
		this.taskFuture = taskFuture;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/GeneratedClasspathScriptProvider.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public abstract class GeneratedClasspathScriptProvider implements ClasspathScriptProvider {
	private final Map<String, Integer> scriptFilepathsToIds = new HashMap<String, Integer>();
	private final Map<Integer, String> scriptIdsToFilepaths = new HashMap<Integer, String>();
	private final Map<Integer, Object> idsToScripts = new HashMap<Integer, Object>();

	public GeneratedClasspathScriptProvider() {
		super();

		final Map<String, Object> generatedScripts = getGeneratedScripts();

		int count = 0;
		for(String filepath : generatedScripts.keySet()) {
			scriptFilepathsToIds.put(filepath, count);
			scriptIdsToFilepaths.put(count, filepath);
			idsToScripts.put(count, generatedScripts.get(filepath));
			count++;
		}
	}

	@Override
	public <T> T getClasspathScript(int scriptId) {
		return (T) idsToScripts.get(scriptId);
	}

	@Override
	public int getScriptId(String filepath) {
		return scriptFilepathsToIds.get(filepath);
	}

	@Override
	public int getTotalScripts() {
		return scriptFilepathsToIds.size();
	}

	@Override
	public Set<String> getFilepaths() {
		return scriptFilepathsToIds.keySet();
	}

	@Override
	public String getFilepath(int scriptId) {
		return scriptIdsToFilepaths.get(scriptId);
	}

	public abstract Map getGeneratedScripts();
}


<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/dummy/DummyJavaObject.java<|end_filename|>
/**
 * Copyright 2016 <NAME>
 */
package org.mini2Dx.miniscript.core.dummy;

/**
 *
 */
public class DummyJavaObject {
	private int value;

	public int getValue() {
		return value;
	}

	public void setValue(int value) {
		this.value = value;
	}
}


<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/ScriptExecutionTaskTest.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core;

import org.junit.Assert;
import org.junit.Test;
import org.mini2Dx.miniscript.core.dummy.DummyGameScriptingEngine;
import org.mini2Dx.miniscript.core.dummy.DummyScript;
import org.mini2Dx.miniscript.core.dummy.DummyScriptExecutor;

/**
 * Unit tests for {@link ScriptExecutionTask}
 */
public class ScriptExecutionTaskTest {

	@Test
	public void testRunExecutesScript() {
		DummyScript script = new DummyScript("");
		ScriptExecutionTask<DummyScript> task = new ScriptExecutionTask<DummyScript>(0, new DummyGameScriptingEngine(),
				new DummyScriptExecutor(null), 0, new GlobalGameScript<DummyScript>(script), new ScriptBindings(), null, true);
		task.run();
		Assert.assertEquals(true, script.isExecuted());
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/util/ToSetConsumer.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import java.util.Set;
import java.util.function.Consumer;

public class ToSetConsumer<T> implements Consumer<T> {
	private Set<T> set;

	@Override
	public void accept(T t) {
		set.add(t);
	}

	public void setSet(Set<T> list) {
		this.set = list;
	}
}


<|start_filename|>ruby/src/main/java/org/mini2Dx/miniscript/ruby/RubyScriptExecutorPool.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.ruby;

import org.jruby.RubyInstanceConfig.CompileMode;
import org.jruby.embed.EmbedEvalUnit;
import org.jruby.embed.LocalContextScope;
import org.jruby.embed.LocalVariableBehavior;
import org.jruby.embed.ScriptingContainer;
import org.mini2Dx.miniscript.core.*;
import org.mini2Dx.miniscript.core.exception.InsufficientCompilersException;
import org.mini2Dx.miniscript.core.exception.NoSuchScriptException;
import org.mini2Dx.miniscript.core.exception.ScriptExecutorUnavailableException;
import org.mini2Dx.miniscript.core.util.ReadWriteBlockingQueue;
import org.mini2Dx.miniscript.core.util.ReadWriteMap;

import java.util.Map;
import java.util.concurrent.BlockingQueue;

/**
 * An implementation of {@link ScriptExecutorPool} for Ruby-based scripts
 */
public class RubyScriptExecutorPool implements ScriptExecutorPool<EmbedEvalUnit> {
	private final Map<Long, ScriptingContainer> threadCompilers = new ReadWriteMap<>();
	private final Map<Integer, PerThreadGameScript<EmbedEvalUnit>> scripts = new ReadWriteMap<>();
	private final Map<String, Integer> filepathToScriptId = new ReadWriteMap<String, Integer>();
	private final Map<Integer, String> scriptIdToFilepath = new ReadWriteMap<Integer, String>();
	private final BlockingQueue<ScriptExecutor<EmbedEvalUnit>> executors;
	private final GameScriptingEngine gameScriptingEngine;
	private final SynchronizedObjectPool<RubyEmbeddedScriptInvoker> embeddedScriptInvokerPool = new SynchronizedObjectPool<RubyEmbeddedScriptInvoker>() {
		@Override
		protected RubyEmbeddedScriptInvoker construct() {
			return new RubyEmbeddedScriptInvoker(gameScriptingEngine, RubyScriptExecutorPool.this);
		}
	};

	public RubyScriptExecutorPool(GameScriptingEngine gameScriptingEngine, int poolSize) {
		this.gameScriptingEngine = gameScriptingEngine;
		executors = new ReadWriteBlockingQueue<>(poolSize);

		for (int i = 0; i < poolSize; i++) {
			executors.offer(new RubyScriptExecutor(this));
		}
	}

	@Override
	public int getCompiledScriptId(String filepath) {
		return filepathToScriptId.getOrDefault(filepath, -1);
	}

	@Override
	public String getCompiledScriptPath(int scriptId) {
		return scriptIdToFilepath.get(scriptId);
	}

	@Override
	public int preCompileScript(String filepath, String scriptContent) throws InsufficientCompilersException {
		PerThreadGameScript<EmbedEvalUnit> script = new PerThreadGameScript<EmbedEvalUnit>(scriptContent);
		scripts.put(script.getId(), script);
		filepathToScriptId.put(filepath, script.getId());
		scriptIdToFilepath.put(script.getId(), filepath);
		return script.getId();
	}

	@Override
	public ScriptExecutionTask<?> execute(int taskId, int scriptId, ScriptBindings scriptBindings,
			ScriptInvocationListener invocationListener, boolean syncCall) {
		ScriptExecutor<EmbedEvalUnit> executor = allocateExecutor();
		if (executor == null) {
			throw new ScriptExecutorUnavailableException(scriptId);
		}
		if(!scripts.containsKey(scriptId)) {
			executor.release();
			throw new NoSuchScriptException(scriptId);
		}
		return new ScriptExecutionTask<EmbedEvalUnit>(taskId, gameScriptingEngine, executor, scriptId, scripts.get(scriptId),
				scriptBindings, invocationListener, syncCall);
	}

	@Override
	public void release(ScriptExecutor<EmbedEvalUnit> executor) {
		try {
			executors.put(executor);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private ScriptExecutor<EmbedEvalUnit> allocateExecutor() {
		try {
			return executors.take();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return null;
	}

	public ScriptingContainer getLocalScriptingContainer() {
		long threadId = Thread.currentThread().getId();
		if (!threadCompilers.containsKey(threadId)) {
			ScriptingContainer scriptingContainer = new ScriptingContainer(LocalContextScope.SINGLETHREAD,
					LocalVariableBehavior.PERSISTENT);
			scriptingContainer.setCompileMode(CompileMode.JIT);
			threadCompilers.put(threadId, scriptingContainer);
		}
		return threadCompilers.get(threadId);
	}

	@Override
	public GameScriptingEngine getGameScriptingEngine() {
		return gameScriptingEngine;
	}

	GameScript<EmbedEvalUnit> getScript(int scriptId) {
		return scripts.get(scriptId);
	}

	public SynchronizedObjectPool<RubyEmbeddedScriptInvoker> getEmbeddedScriptInvokerPool() {
		return embeddedScriptInvokerPool;
	}
}


<|start_filename|>lua/src/main/java/org/mini2Dx/miniscript/lua/LuaScriptExecutorPool.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.lua;

import org.luaj.vm2.Globals;
import org.luaj.vm2.LoadState;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.compiler.LuaC;
import org.luaj.vm2.lib.*;
import org.luaj.vm2.lib.jse.JseBaseLib;
import org.luaj.vm2.lib.jse.JseMathLib;
import org.luaj.vm2.lib.jse.JsePlatform;
import org.mini2Dx.miniscript.core.*;
import org.mini2Dx.miniscript.core.exception.InsufficientCompilersException;
import org.mini2Dx.miniscript.core.exception.NoSuchScriptException;
import org.mini2Dx.miniscript.core.exception.ScriptExecutorUnavailableException;
import org.mini2Dx.miniscript.core.util.ReadWriteBlockingQueue;
import org.mini2Dx.miniscript.core.util.ReadWriteMap;

import java.util.Map;
import java.util.concurrent.BlockingQueue;

/**
 * An implementation of {@link ScriptExecutorPool} for Lua scripts
 */
public class LuaScriptExecutorPool implements ScriptExecutorPool<LuaValue> {
	private final Map<Long, Globals> threadCompilers = new ReadWriteMap<>();
	private final Map<Integer, GameScript<LuaValue>> scripts = new ReadWriteMap<Integer, GameScript<LuaValue>>();
	private final Map<String, Integer> filepathToScriptId = new ReadWriteMap<String, Integer>();
	private final Map<Integer, String> scriptIdToFilepath = new ReadWriteMap<Integer, String>();
	private final BlockingQueue<ScriptExecutor<LuaValue>> executors;
	private final GameScriptingEngine gameScriptingEngine;
	private final ClasspathScriptProvider classpathScriptProvider;
	private final boolean sandboxed;
	private final SynchronizedObjectPool<LuaEmbeddedScriptInvoker> embeddedScriptInvokerPool = new SynchronizedObjectPool<LuaEmbeddedScriptInvoker>() {
		@Override
		protected LuaEmbeddedScriptInvoker construct() {
			return new LuaEmbeddedScriptInvoker(gameScriptingEngine, LuaScriptExecutorPool.this);
		}
	};
	
	private Globals sandboxedGlobals;

	public LuaScriptExecutorPool(GameScriptingEngine gameScriptingEngine,
	                             ClasspathScriptProvider classpathScriptProvider,
	                             int poolSize, boolean sandboxed) {
		this.gameScriptingEngine = gameScriptingEngine;
		this.classpathScriptProvider = classpathScriptProvider;
		this.sandboxed = sandboxed;

		if(classpathScriptProvider.getTotalScripts() > 0) {
			for(int i = 0; i < classpathScriptProvider.getTotalScripts(); i++) {
				GameScript.offsetIds(i - 1);
				PerThreadClasspathGameScript testing = new PerThreadClasspathGameScript<LuaValue>((LuaValue) classpathScriptProvider.getClasspathScript(i));
				int key = testing.getId();
				scripts.put(key, testing);
				String filepath = classpathScriptProvider.getFilepath(key);
				filepathToScriptId.put(filepath, key);
				scriptIdToFilepath.put(key, filepath);
			}
		}
		
		executors = new ReadWriteBlockingQueue<>(poolSize);

		for (int i = 0; i < poolSize; i++) {
			executors.offer(new LuaScriptExecutor(this));
		}
		
		if(sandboxed) {
			//Create sandboxed compiler
			sandboxedGlobals = new Globals();
			sandboxedGlobals.load(new JseBaseLib());
			sandboxedGlobals.load(new PackageLib());
			sandboxedGlobals.load(new StringLib());

			// To load scripts, we occasionally need a math library in addition to compiler support.
			// To limit scripts using the debug library, they must be closures, so we only install LuaC.
			sandboxedGlobals.load(new JseMathLib());
			LoadState.install(sandboxedGlobals);
			LuaC.install(sandboxedGlobals);
		}
	}

	@Override
	public int getCompiledScriptId(String filepath) {
		return filepathToScriptId.getOrDefault(filepath, -1);
	}

	@Override
	public String getCompiledScriptPath(int scriptId) {
		return scriptIdToFilepath.get(scriptId);
	}

	@Override
	public int preCompileScript(String filepath, String scriptContent) throws InsufficientCompilersException {
		PerThreadGameScript<LuaValue> script = new PerThreadGameScript<LuaValue>(scriptContent);
		scripts.put(script.getId(), script);
		filepathToScriptId.put(filepath, script.getId());
		scriptIdToFilepath.put(script.getId(), filepath);
		return script.getId();
	}

	@Override
	public ScriptExecutionTask<?> execute(int taskId, int scriptId, ScriptBindings scriptBindings,
			ScriptInvocationListener invocationListener, boolean syncCall) {
		ScriptExecutor<LuaValue> executor = allocateExecutor();
		if (executor == null) {
			throw new ScriptExecutorUnavailableException(scriptId);
		}
		if(!scripts.containsKey(scriptId)) {
			executor.release();
			throw new NoSuchScriptException(scriptId);
		}
		return new ScriptExecutionTask<LuaValue>(taskId,
				gameScriptingEngine, executor, scriptId, scripts.get(scriptId),
				scriptBindings, invocationListener, syncCall);
	}

	@Override
	public void release(ScriptExecutor<LuaValue> executor) {
		try {
			executors.put(executor);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	@Override
	public GameScriptingEngine getGameScriptingEngine() {
		return gameScriptingEngine;
	}

	GameScript<LuaValue> getScript(int id) {
		return scripts.get(id);
	}
	
	private ScriptExecutor<LuaValue> allocateExecutor() {
		try {
			return executors.take();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public Globals getLocalGlobals() {
		long threadId = Thread.currentThread().getId();
		if (!threadCompilers.containsKey(threadId)) {
			if(sandboxed) {
				threadCompilers.put(threadId, createSandboxedGlobals());
			} else {
				threadCompilers.put(threadId, JsePlatform.standardGlobals());
			}
		}
		return threadCompilers.get(threadId);
	}

	public LuaValue compileWithGlobals(Globals globals, GameScript<LuaValue> gameScript) {
		if(gameScript instanceof GlobalGameScript) {
			return gameScript.getScript();
		} else if(gameScript instanceof PerThreadClasspathGameScript) {
			return compileClassPathGameScript(globals, (PerThreadClasspathGameScript) gameScript);
		} else {
			return compilePerThreadGameScriptWithGlobals(globals, (PerThreadGameScript) gameScript);
		}
	}

	private LuaValue compileClassPathGameScript(Globals globals, PerThreadClasspathGameScript<LuaValue> perThreadClasspathGameScript) {
		final LuaValue result = perThreadClasspathGameScript.compileInstance();
		result.initupvalue1(globals);
		return result;
	}

	private LuaValue compilePerThreadGameScriptWithGlobals(Globals globals, PerThreadGameScript<LuaValue> perThreadGameScript) {
		if(sandboxed) {
			return sandboxedGlobals.load(perThreadGameScript.getContent(), "main", globals);
		} else {
			return globals.load(perThreadGameScript.getContent());
		}
	}
	
	private Globals createSandboxedGlobals() {
		Globals result = new Globals();
		result.load(new JseBaseLib());
		result.load(new PackageLib());
		result.load(new Bit32Lib());
		result.load(new TableLib());
		result.load(new StringLib());
		result.load(new JseMathLib());
		result.load(new DebugLib());
		result.set("debug", LuaValue.NIL);
		return result;
	}

	public SynchronizedObjectPool<LuaEmbeddedScriptInvoker> getEmbeddedScriptInvokerPool() {
		return embeddedScriptInvokerPool;
	}
}


<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/util/ScriptInvocationQueueTest.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import org.junit.Assert;
import org.junit.Test;
import org.mini2Dx.miniscript.core.*;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class ScriptInvocationQueueTest implements ScriptInvocationListener {
	private static final int INTERACTIVE_SCRIPT_ID = 1;

	private final ScriptInvocationQueue invocationQueue = new ScriptInvocationQueue();
	private final ScriptInvocationPool scriptInvocationPool = new ScriptInvocationPool();
	private final AtomicInteger interactiveScriptsRunning = new AtomicInteger();

	@Test
	public void testOnlyOneInteractiveScriptPolled() {
		final Thread [] threads = new Thread[4];

		for(int i = 0; i < 1000; i++) {
			invocationQueue.offer(createInvocation(i % 10 == 0 ? INTERACTIVE_SCRIPT_ID : i, i % 10 == 0));
		}

		final CountDownLatch countDownLatch = new CountDownLatch(threads.length);

		for(int i = 0; i < threads.length; i++) {
			threads[i] = new Thread(() -> {
				countDownLatch.countDown();
				try {
					countDownLatch.await();
				} catch (Exception e) {}

				while(!invocationQueue.isEmpty()) {
					ScriptInvocation scriptInvocation = invocationQueue.poll();
					if(scriptInvocation == null) {
						continue;
					}
					if(scriptInvocation.isInteractive()) {
						interactiveScriptsRunning.incrementAndGet();
						Assert.assertTrue(invocationQueue.isInteractiveScriptRunnung());
					}
					try {
						Thread.sleep(10);
					} catch (Exception e) {}
					scriptInvocation.getInvocationListener().onScriptSkipped(scriptInvocation.getScriptId());
					scriptInvocationPool.release(scriptInvocation);
				}
			});
		}
		for(int i = 0; i < threads.length; i++) {
			threads[i].start();
		}
		while(!invocationQueue.isEmpty()) {
			Assert.assertTrue(interactiveScriptsRunning.get() <= 1);
		}
		for(int i = 0; i < threads.length; i++) {
			try {
				threads[i].join();
			} catch (InterruptedException e) {}
		}
		Assert.assertFalse(invocationQueue.isInteractiveScriptRunnung());
	}

	private ScriptInvocation createInvocation(int scriptId, boolean interactive) {
		return scriptInvocationPool.allocate(scriptId, new ScriptBindings(), this,  0, interactive);
	}

	@Override
	public void onScriptSuccess(int scriptId, ScriptExecutionResult executionResult) {
		if(scriptId == INTERACTIVE_SCRIPT_ID) {
			interactiveScriptsRunning.decrementAndGet();
			invocationQueue.clearInteractiveScriptStatus();
		}
	}

	@Override
	public void onScriptSkipped(int scriptId) {
		if(scriptId == INTERACTIVE_SCRIPT_ID) {
			interactiveScriptsRunning.decrementAndGet();
			invocationQueue.clearInteractiveScriptStatus();
		}
	}

	@Override
	public void onScriptException(int scriptId, Exception e) {
		if(scriptId == INTERACTIVE_SCRIPT_ID) {
			interactiveScriptsRunning.decrementAndGet();
			invocationQueue.clearInteractiveScriptStatus();
		}
	}

	@Override
	public boolean callOnGameThread() {
		return false;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/notification/ScriptBeginNotification.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.notification;

import org.mini2Dx.miniscript.core.ScriptInvocationListener;

import java.util.concurrent.atomic.AtomicBoolean;

public class ScriptBeginNotification implements ScriptNotification {
	private final AtomicBoolean processed = new AtomicBoolean(false);
	private final ScriptInvocationListener invocationListener;
	private final int scriptId;

	public ScriptBeginNotification(ScriptInvocationListener invocationListener, int scriptId) {
		this.invocationListener = invocationListener;
		this.scriptId = scriptId;
	}
	@Override
	public void process() {
		invocationListener.onScriptBegin(scriptId);
		processed.set(true);

		synchronized(this) {
			this.notifyAll();
		}
	}

	public boolean isProcessed() {
		return processed.get();
	}
}


<|start_filename|>core/src/test/java/org/mini2Dx/miniscript/core/ScriptInvocationTest.java<|end_filename|>
/**
 * Copyright 2020 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core;

import org.junit.Assert;
import org.junit.Test;

import java.util.*;
import java.util.concurrent.PriorityBlockingQueue;

public class ScriptInvocationTest {
	private final PriorityBlockingQueue<ScriptInvocation> queue = new PriorityBlockingQueue<>();

	@Test
	public void testPriority1() {
		final ScriptInvocation invocation1 = new ScriptInvocation(null);
		final ScriptInvocation invocation2 = new ScriptInvocation(null);

		invocation1.setPriority(0);
		invocation2.setPriority(1000);

		queue.offer(invocation1);
		queue.offer(invocation2);

		Assert.assertEquals(invocation2, queue.poll());
		Assert.assertEquals(invocation1, queue.poll());

		queue.offer(invocation2);
		queue.offer(invocation1);

		Assert.assertEquals(invocation2, queue.poll());
		Assert.assertEquals(invocation1, queue.poll());
	}

	@Test
	public void testPriority2() {
		final ScriptInvocation invocation1 = new ScriptInvocation(null);
		final ScriptInvocation invocation2 = new ScriptInvocation(null);

		invocation1.setPriority(9999);
		invocation2.setPriority(100);

		queue.offer(invocation1);
		queue.offer(invocation2);

		Assert.assertEquals(invocation1, queue.poll());
		Assert.assertEquals(invocation2, queue.poll());

		queue.offer(invocation2);
		queue.offer(invocation1);

		Assert.assertEquals(invocation1, queue.poll());
		Assert.assertEquals(invocation2, queue.poll());
	}

	@Test
	public void testSamePriority() {
		final ScriptInvocation invocation1 = new ScriptInvocation(null);
		final ScriptInvocation invocation2 = new ScriptInvocation(null);

		invocation1.setPriority(0);
		invocation2.setPriority(0);

		queue.offer(invocation1);
		queue.offer(invocation2);

		Assert.assertEquals(invocation1, queue.poll());
		Assert.assertEquals(invocation2, queue.poll());

		queue.offer(invocation2);
		queue.offer(invocation1);

		Assert.assertEquals(invocation2, queue.poll());
		Assert.assertEquals(invocation1, queue.poll());
	}

	@Test
	public void testSamePriorityWithTimestamp() {
		final ScriptInvocation invocation1 = new ScriptInvocation(null);
		final ScriptInvocation invocation2 = new ScriptInvocation(null);

		invocation1.setPriority(0);
		invocation2.setPriority(0);

		invocation1.setInvokeTimestamp(System.nanoTime());
		invocation2.setInvokeTimestamp(invocation1.getInvokeTimestamp() - 1);

		queue.offer(invocation1);
		queue.offer(invocation2);

		Assert.assertEquals(invocation2, queue.poll());
		Assert.assertEquals(invocation1, queue.poll());

		queue.offer(invocation2);
		queue.offer(invocation1);

		Assert.assertEquals(invocation2, queue.poll());
		Assert.assertEquals(invocation1, queue.poll());
	}

	@Test
	public void testSamePriorityMultiple() {
		final List<ScriptInvocation> expectedResults = new ArrayList<>();

		int taskId = 0;

		for(int priority = 0; priority < 10; priority++) {
			List<ScriptInvocation> invocations = new ArrayList<>();

			for(int i = 0; i < 10; i++) {
				final ScriptInvocation invocation = new ScriptInvocation(null);
				invocation.setTaskId(taskId);
				invocation.setPriority(priority);
				invocation.setInvokeTimestamp(System.nanoTime());
				queue.offer(invocation);
				invocations.add(invocation);
				taskId++;
			}

			Collections.reverse(invocations);
			expectedResults.addAll(invocations);
		}

		Collections.reverse(expectedResults);

		long previousTimestamp = Long.MIN_VALUE;
		for(int priority = 0; priority < 10; priority++) {
			previousTimestamp = Long.MIN_VALUE;

			for(int i = 0; i < 10; i++) {
				final ScriptInvocation expected = expectedResults.remove(0);
				final ScriptInvocation actual = queue.poll();

				Assert.assertTrue(actual.getInvokeTimestamp() > previousTimestamp);
				previousTimestamp = actual.getInvokeTimestamp();

				Assert.assertEquals(9 - priority, actual.getPriority());
				Assert.assertEquals(expected.getPriority(), actual.getPriority());
				Assert.assertEquals(expected.getTaskId(), actual.getTaskId());
			}
		}
	}

	@Test
	public void testPriority() {
		final Random random = new Random();
		final List<Integer> unordered = new ArrayList<>();

		for(int i = 0; i < 1000; i++) {
			final int priority = random.nextInt();
			final ScriptInvocation invocation = new ScriptInvocation(null);
			invocation.setPriority(priority);
			unordered.add(priority);

			queue.offer(invocation);
		}

		final List<Integer> ordered = new ArrayList<>(unordered);
		Collections.sort(ordered);
		Collections.reverse(ordered);

		for(int i = 0; i < 1000; i++) {
			final int expectedPriority = ordered.get(i);
			final ScriptInvocation result = queue.poll();
			Assert.assertEquals(expectedPriority, result.getPriority());
		}
	}
}


<|start_filename|>groovy/src/test/resources/waitForCompletion.groovy<|end_filename|>
stringValue += "123";
intValue = 101;
booleanValue = true;
future.waitForCompletion();

<|start_filename|>gradle-plugin/src/main/java/org/mini2Dx/miniscript/gradle/MiniscriptStubTask.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.gradle;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import org.mini2Dx.miniscript.core.GeneratedClasspathScriptProvider;

import javax.lang.model.element.Modifier;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MiniscriptStubTask extends MiniscriptTask {
	private File packageDir;

	@Override
	protected void process(List<CompilerInputFile> inputFiles) throws IOException {
		packageDir = new File(outputDir, outputPackage.replace('.', '/'));

		for(CompilerInputFile inputFile : inputFiles) {
			generateScriptStub(inputFile);
		}

		generateScriptProvider(inputFiles);
	}

	private void generateScriptStub(CompilerInputFile inputFile) throws IOException {
		TypeSpec classSpec = TypeSpec.classBuilder(inputFile.getOutputClassName())
				.addModifiers(Modifier.PUBLIC)
				.build();

		final String scriptPackage = inputFile.getOutputPackageName(outputPackage);

		JavaFile javaFile = JavaFile.builder(scriptPackage, classSpec)
				.addFileComment("Auto-generated by miniscript-gradle-plugin")
				.build();

		javaFile.writeTo(outputDir);
	}

	private void generateScriptProvider(List<CompilerInputFile> inputFiles) throws IOException {
		final String outputClassName = outputClass.get().substring(outputClass.get().lastIndexOf('.') + 1);

		MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("getGeneratedScripts")
				.addModifiers(Modifier.PUBLIC)
				.returns(Map.class)
				.addStatement("final $T result = new $T()", Map.class, HashMap.class);

		for(CompilerInputFile inputFile : inputFiles) {
			final String scriptPackage = inputFile.getOutputPackageName(outputPackage);
			final ClassName scriptClassName = ClassName.get(scriptPackage, inputFile.getOutputClassName());
			final String path;
			if(getPrefixWithRoot().getOrElse(false)) {
				path = scriptsDir.get().getAsFile().getName() + "/" + inputFile.getInputScriptRelativeFilename().replace('\\', '/');
			} else {
				path =  inputFile.getInputScriptRelativeFilename().replace('\\', '/');
			}
			methodBuilder = methodBuilder.addStatement("result.put($S, new $T())", path, scriptClassName);
		}

		methodBuilder = methodBuilder.addStatement("return result");
		final MethodSpec methodSpec = methodBuilder.build();

		final TypeSpec classSpec = TypeSpec.classBuilder(outputClassName)
				.addModifiers(Modifier.PUBLIC)
				.superclass(GeneratedClasspathScriptProvider.class)
				.addMethod(methodSpec)
				.build();

		JavaFile javaFile = JavaFile.builder(outputPackage, classSpec)
				.addFileComment("Auto-generated by miniscript-gradle-plugin")
				.build();

		javaFile.writeTo(outputDir);
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/util/ScheduledTaskQueue.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import org.mini2Dx.miniscript.core.threadpool.ScheduledTask;

import java.util.concurrent.TimeUnit;

public class ScheduledTaskQueue extends ReadWritePriorityQueue<ScheduledTask> {

	public ScheduledTaskQueue() {
		this(Integer.MAX_VALUE);
	}

	public ScheduledTaskQueue(int maxCapacity) {
		super(maxCapacity);
	}

	@Override
	public ScheduledTask take() throws InterruptedException {
		ScheduledTask head = super.peek();
		while(head == null || head.getScheduledStartTimeNanos() > System.nanoTime()) {
			if(head == null) {
				synchronized(waitingForItemsMonitor) {
					waitingForItemsMonitor.wait();
				}
			} else {
				long currentTimeNanos = System.nanoTime();
				long delayMillis = Math.max (0, (head.getScheduledStartTimeNanos() - currentTimeNanos) / TimeUnit.MILLISECONDS.toNanos(1));
				int delayNanos = Math.max (0, (int) ((head.getScheduledStartTimeNanos() - currentTimeNanos) % TimeUnit.MILLISECONDS.toNanos(1)));

				synchronized(waitingForItemsMonitor) {
					waitingForItemsMonitor.wait(delayMillis, delayNanos);
				}
			}

			head = super.peek();
		}

		lock.lockWrite();
		ScheduledTask result = super.peek();
		while (isEmpty() || result == null || result.getScheduledStartTimeNanos() > System.nanoTime()) {
			lock.unlockWrite();
			synchronized(waitingForItemsMonitor) {
				if(result == null) {
					waitingForItemsMonitor.wait();
				} else {
					long currentTimeNanos = System.nanoTime();
					long delayMillis = Math.max (0, (head.getScheduledStartTimeNanos() - currentTimeNanos) / TimeUnit.MILLISECONDS.toNanos(1));
					int delayNanos = Math.max (0, (int) ((head.getScheduledStartTimeNanos() - currentTimeNanos) % TimeUnit.MILLISECONDS.toNanos(1)));
					waitingForItemsMonitor.wait(delayMillis, delayNanos);
				}
			}
			lock.lockWrite();
			result = super.peek();
		}
		result = super.poll();
		lock.unlockWrite();

		synchronized(waitingForRemovalMonitor) {
			waitingForRemovalMonitor.notify();
		}
		return result;
	}
}


<|start_filename|>lua/src/test/resources/illegalSandbox.lua<|end_filename|>
stringValue = stringValue .. "123"
intValue = 101
booleanValue = true
jscanner = luajava.bindClass( "java.util.Scanner" )
scanner = luajava.newInstance( "java.util.Scanner", "Test" );

<|start_filename|>gradle-plugin/src/main/java/org/mini2Dx/miniscript/gradle/MiniscriptTask.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.gradle;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public abstract class MiniscriptTask extends DefaultTask {
	private final Property<Boolean> prefixWithRoot;
	protected final Property<Boolean> recursive;
	protected final Property<String> outputClass;
	protected final DirectoryProperty scriptsDir;

	protected File outputDir;

	protected String outputPackage;

	public MiniscriptTask() {
		super();
		prefixWithRoot = getProject().getObjects().property(Boolean.class);
		recursive = getProject().getObjects().property(Boolean.class);
		outputClass = getProject().getObjects().property(String.class);
		scriptsDir = getProject().getLayout().directoryProperty();
	}

	@TaskAction
	public void run() throws IOException {
		outputPackage = outputClass.get().substring(0, outputClass.get().lastIndexOf('.'));

		final List<CompilerInputFile> inputFiles = new ArrayList<CompilerInputFile>();
		listScriptFiles(inputFiles, scriptsDir.get().getAsFile());
		process(inputFiles);
	}

	private void listScriptFiles(List<CompilerInputFile> result, File inputDirectory) throws IOException {
		for(File file : inputDirectory.listFiles()) {
			if(file.isDirectory()) {
				if(recursive.get()) {
					listScriptFiles(result, file);
				}
				continue;
			}

			result.add(new CompilerInputFile(scriptsDir.get().getAsFile(), file));
		}
	}

	protected abstract void process(List<CompilerInputFile> inputFiles) throws IOException;

	@InputDirectory
	public DirectoryProperty getScriptsDir() {
		return scriptsDir;
	}

	@Input
	public Property<Boolean> getPrefixWithRoot() {
		return prefixWithRoot;
	}

	@Input
	public Property<Boolean> getRecursive() {
		return recursive;
	}

	@Input
	public Property<String> getOutputClass() {
		return outputClass;
	}

	@OutputDirectory
	public File getOutputDir() {
		return outputDir;
	}

	public void setOutputDir(File outputDir) {
		this.outputDir = outputDir;
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/threadpool/KavaThreadPoolProvider.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core.threadpool;

import org.mini2Dx.miniscript.core.ThreadPoolProvider;
import org.mini2Dx.miniscript.core.util.ScheduledTaskQueue;

import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * {@link ThreadPoolProvider} implementation for Kava-based runtimes
 */
public class KavaThreadPoolProvider implements Runnable, ThreadPoolProvider {
	private final AtomicBoolean running = new AtomicBoolean(true);
	private final Thread [] threads;

	private final ScheduledTaskQueue scheduledTaskQueue = new ScheduledTaskQueue();

	public KavaThreadPoolProvider() {
		this(Runtime.getRuntime().availableProcessors() + 1);
	}

	public KavaThreadPoolProvider(int maxConcurrentScripts) {
		threads = new Thread[maxConcurrentScripts];
		for(int i = 0; i < maxConcurrentScripts; i++) {
			threads[i] = new Thread(this);
		}
		for(int i = 0; i < maxConcurrentScripts; i++) {
			threads[i].start();
		}
	}

	@Override
	public Future<?> submit(Runnable task) {
		return schedule(task, 0L, TimeUnit.MILLISECONDS);
	}

	@Override
	public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
		final ScheduledTask scheduledTask = ScheduledTask.allocate(command, System.nanoTime() + unit.toNanos(initialDelay),
				period, unit);
		final ScheduledTaskFuture future = new ScheduledTaskFuture(scheduledTaskQueue);

		scheduledTask.setFuture(future);
		future.setScheduledTask(scheduledTask);

		scheduledTaskQueue.offer(scheduledTask);
		return future;
	}

	@Override
	public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
		final ScheduledTask scheduledTask = ScheduledTask.allocate(command, System.nanoTime() + unit.toNanos(delay));
		final ScheduledTaskFuture future = new ScheduledTaskFuture(scheduledTaskQueue);

		scheduledTask.setFuture(future);
		future.setScheduledTask(scheduledTask);

		scheduledTaskQueue.offer(scheduledTask);
		return future;
	}

	@Override
	public void shutdown(boolean interruptThreads) {
		running.set(false);

		if (!interruptThreads) {
			return;
		}
		for (int i = 0; i < threads.length; i++) {
			threads[i].interrupt();
		}
	}

	@Override
	public void run() {
		final Thread currentThread = Thread.currentThread();
		while(running.get()) {
			final ScheduledTask scheduledTask;
			try {
				scheduledTask = scheduledTaskQueue.take();
				if(scheduledTask.getScheduledStartTimeNanos() > System.nanoTime()) {

				}
			} catch (InterruptedException e) {
				e.printStackTrace();
				continue;
			}
			if(scheduledTask == null) {
				continue;
			}
			try {
				final ScheduledTaskFuture future = scheduledTask.getFuture();
				future.setExecutingThread(currentThread);
				final Runnable runnable = scheduledTask.getRunnable();
				if(runnable != null && !future.isCancelled()) {
					runnable.run();
				}
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				final ScheduledTaskFuture future = scheduledTask.getFuture();
				if(scheduledTask.isRepeating() && !future.isCancelled()) {
					final ScheduledTask nextTask = ScheduledTask.allocate(scheduledTask.getRunnable(),
							scheduledTask.getScheduledStartTimeNanos() + scheduledTask.getRepeatUnit().toNanos(scheduledTask.getRepeatInterval()),
							scheduledTask.getRepeatInterval(), scheduledTask.getRepeatUnit());

					nextTask.setFuture(future);
					future.setScheduledTask(nextTask);

					scheduledTaskQueue.offer(nextTask);
				} else {
					future.markDone();
				}
				scheduledTask.dispose();
			}
		}
	}
}


<|start_filename|>groovy/src/main/java/org/mini2Dx/miniscript/groovy/GroovyScriptExecutor.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.groovy;

import org.mini2Dx.miniscript.core.*;

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;

/**
 * An implementation of {@link ScriptExecutor} for Groovy-based scripts
 */
public class GroovyScriptExecutor implements ScriptExecutor<Script> {
	private final GroovyScriptExecutorPool executorPool;
	private final GroovyShell groovyShell = new GroovyShell();

	private Script lastScript;
	
	public GroovyScriptExecutor(GroovyScriptExecutorPool executorPool) {
		this.executorPool = executorPool;
	}

	@Override
	public GameScript<Script> compile(String script) {
		return new GlobalGameScript<Script>(groovyShell.parse(script));
	}
	
	@Override
	public ScriptExecutionResult execute(int scriptId, GameScript<Script> script, ScriptBindings bindings, boolean returnResult) throws Exception {
		final Script groovyScript = script.getScript();
		this.lastScript = groovyScript;

		final GroovyEmbeddedScriptInvoker embeddedScriptInvoker = executorPool.getEmbeddedScriptInvokerPool().allocate();
		embeddedScriptInvoker.setScriptBindings(bindings);
		embeddedScriptInvoker.setScriptExecutor(this);
		embeddedScriptInvoker.setParentScriptId(scriptId);

		final Binding binding = new Binding(bindings);
		binding.setVariable(ScriptBindings.SCRIPT_PARENT_ID_VAR, -1);
		binding.setVariable(ScriptBindings.SCRIPT_ID_VAR, scriptId);
		binding.setVariable(ScriptBindings.SCRIPT_INVOKE_VAR, embeddedScriptInvoker);
		groovyScript.setBinding(binding);
		groovyScript.run();

		executorPool.getEmbeddedScriptInvokerPool().release(embeddedScriptInvoker);
		
		return returnResult ? new ScriptExecutionResult(groovyScript.getBinding().getVariables()) : null;
	}

	@Override
	public void executeEmbedded(int parentScriptId, int scriptId, GameScript<Script> script,
								EmbeddedScriptInvoker embeddedScriptInvoker, ScriptBindings bindings) throws Exception {
		final Script previousScript = lastScript;
		final Script groovyScript = script.getScript();
		this.lastScript = groovyScript;
		embeddedScriptInvoker.setParentScriptId(scriptId);

		final Binding binding = new Binding(previousScript.getBinding().getVariables());
		binding.setVariable(ScriptBindings.SCRIPT_PARENT_ID_VAR, parentScriptId);
		binding.setVariable(ScriptBindings.SCRIPT_ID_VAR, scriptId);
		binding.setVariable(ScriptBindings.SCRIPT_INVOKE_VAR, embeddedScriptInvoker);
		groovyScript.setBinding(binding);
		groovyScript.run();

		this.lastScript = previousScript;
		lastScript.getBinding().setVariable(ScriptBindings.SCRIPT_ID_VAR, parentScriptId);
		embeddedScriptInvoker.setParentScriptId(parentScriptId);

		for(Object variableName : groovyScript.getBinding().getVariables().keySet()) {
			this.lastScript.getBinding().setVariable((String) variableName, groovyScript.getBinding().getVariables().get(variableName));
		}
	}

	@Override
	public void release() {
		executorPool.release(this);
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/threadpool/ScheduledTaskFuture.java<|end_filename|>
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.core.threadpool;

import org.mini2Dx.miniscript.core.util.ReadWritePriorityQueue;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

public class ScheduledTaskFuture<T extends Object> implements ScheduledFuture<T> {
	private final Object monitor = new Object();
	private final AtomicBoolean done = new AtomicBoolean(false);
	private final AtomicBoolean cancelled = new AtomicBoolean(false);
	private final AtomicReference<Thread> executingThread = new AtomicReference<>(null);

	private final ReadWritePriorityQueue<ScheduledTask> scheduledTaskQueue;

	private ScheduledTask scheduledTask;
	private long scheduledTimeNanos;


	public ScheduledTaskFuture(ReadWritePriorityQueue<ScheduledTask> scheduledTaskQueue) {
		this.scheduledTaskQueue = scheduledTaskQueue;
	}

	public void markDone() {
		executingThread.set(null);
		done.set(true);

		synchronized(monitor) {
			monitor.notifyAll();
		}
	}

	@Override
	public long getDelay(TimeUnit unit) {
		final long delayNanos = System.nanoTime() - scheduledTimeNanos;
		if(delayNanos < 0) {
			return 0L;
		}
		return unit.convert(delayNanos, TimeUnit.NANOSECONDS);
	}

	@Override
	public boolean cancel(boolean mayInterruptIfRunning) {
		cancelled.set(true);

		if(scheduledTask != null) {
			scheduledTaskQueue.remove(scheduledTask);
		}

		if(mayInterruptIfRunning) {
			final Thread thread = executingThread.getAndSet(null);
			if(thread != null) {
				thread.interrupt();
			}
		}

		synchronized(monitor) {
			monitor.notifyAll();
		}
		return false;
	}

	@Override
	public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
		while(!done.get() && !cancelled.get()) {
			synchronized(monitor) {
				monitor.wait(unit.toMillis(timeout));
			}
		}
		return null;
	}

	@Override
	public T get() throws InterruptedException, ExecutionException {
		try {
			return get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
		} catch (TimeoutException e) {
			throw new ExecutionException(e);
		}
	}

	@Override
	public int compareTo(Delayed o) {
		return Long.compare(getDelay(TimeUnit.NANOSECONDS), o.getDelay(TimeUnit.NANOSECONDS));
	}

	@Override
	public boolean isCancelled() {
		return cancelled.get();
	}

	@Override
	public boolean isDone() {
		return done.get();
	}

	public void setScheduledTask(ScheduledTask scheduledTask) {
		this.scheduledTask = scheduledTask;
		this.scheduledTimeNanos = scheduledTask.getScheduledStartTimeNanos();
	}

	public void setExecutingThread(Thread thread) {
		executingThread.set(thread);
	}
}


<|start_filename|>core/src/main/java/org/mini2Dx/miniscript/core/util/ScriptInvocationQueue.java<|end_filename|>
/**
 * Copyright 2021 Viridian Software Ltd.
 */
package org.mini2Dx.miniscript.core.util;

import org.mini2Dx.lockprovider.ReadWriteLock;
import org.mini2Dx.lockprovider.ReentrantLock;
import org.mini2Dx.miniscript.core.GameScriptingEngine;
import org.mini2Dx.miniscript.core.ScriptInvocation;

import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;

public class ScriptInvocationQueue extends AbstractConcurrentBlockingQueue<ScriptInvocation> {
	private final AtomicBoolean interactiveScriptRunning = new AtomicBoolean(false);

	protected final ReadWriteLock interactiveScriptLock = GameScriptingEngine.LOCK_PROVIDER.newReadWriteLock();
	protected final Queue<ScriptInvocation> interactiveScriptQueue = new PriorityQueue<>();;

	public ScriptInvocationQueue() {
		super(Integer.MAX_VALUE, new PriorityQueue<ScriptInvocation>());
	}

	@Override
	public ScriptInvocation poll() {
		ScriptInvocation result = null;
		if(!interactiveScriptRunning.get()) {
			result = pollInteractiveScript();
		}
		if(result == null) {
			result = super.poll();
		}
		return result;
	}

	private ScriptInvocation pollInteractiveScript() {
		interactiveScriptLock.lockRead();
		boolean scriptRunning = interactiveScriptRunning.get();
		boolean queueEmpty = interactiveScriptQueue.isEmpty();
		interactiveScriptLock.unlockRead();

		if(scriptRunning) {
			return null;
		}
		if(queueEmpty) {
			return null;
		}

		ScriptInvocation result = null;
		interactiveScriptLock.lockWrite();
		if(!interactiveScriptRunning.get()) {
			result = interactiveScriptQueue.poll();
			if(result != null) {
				interactiveScriptRunning.set(true);
			}
		}
		interactiveScriptLock.unlockWrite();
		return result;
	}

	@Override
	public boolean offer(ScriptInvocation scriptInvocation) {
		if(scriptInvocation.isInteractive()) {
			interactiveScriptLock.lockWrite();
			boolean result = interactiveScriptQueue.offer(scriptInvocation);
			interactiveScriptLock.unlockWrite();
			return result;
		}
		return super.offer(scriptInvocation);
	}

	@Override
	public int size() {
		interactiveScriptLock.lockRead();
		final int result = interactiveScriptQueue.size();
		interactiveScriptLock.unlockRead();
		return result + super.size();
	}

	@Override
	public boolean isEmpty() {
		return size() == 0;
	}

	@Override
	public void clear() {
		interactiveScriptLock.lockWrite();
		interactiveScriptQueue.clear();
		interactiveScriptLock.unlockWrite();
		super.clear();
	}

	public void clear(List<ScriptInvocation> results) {
		interactiveScriptLock.lockWrite();
		results.addAll(interactiveScriptQueue);
		interactiveScriptQueue.clear();
		interactiveScriptLock.unlockWrite();

		lock.lockWrite();
		results.addAll(internalQueue);
		internalQueue.clear();
		lock.unlockWrite();
	}

	public void cancelByScriptId(int scriptId, List<ScriptInvocation> results) {
		interactiveScriptLock.lockWrite();
		interactiveScriptQueue.removeIf(new Predicate<ScriptInvocation>() {
			@Override
			public boolean test(ScriptInvocation scriptInvocation) {
				if(scriptInvocation.getScriptId() == scriptId) {
					results.add(scriptInvocation);
					return true;
				}
				return false;
			}
		});
		interactiveScriptLock.unlockWrite();

		lock.lockWrite();
		internalQueue.removeIf(new Predicate<ScriptInvocation>() {
			@Override
			public boolean test(ScriptInvocation scriptInvocation) {
				if(scriptInvocation.getScriptId() == scriptId) {
					results.add(scriptInvocation);
					return true;
				}
				return false;
			}
		});
		lock.unlockWrite();
	}

	public void cancelByTaskId(int taskId, List<ScriptInvocation> results) {
		interactiveScriptLock.lockWrite();
		interactiveScriptQueue.removeIf(new Predicate<ScriptInvocation>() {
			@Override
			public boolean test(ScriptInvocation scriptInvocation) {
				if(scriptInvocation.getTaskId() == taskId) {
					results.add(scriptInvocation);
					return true;
				}
				return false;
			}
		});
		interactiveScriptLock.unlockWrite();

		lock.lockWrite();
		internalQueue.removeIf(new Predicate<ScriptInvocation>() {
			@Override
			public boolean test(ScriptInvocation scriptInvocation) {
				if(scriptInvocation.getTaskId() == taskId) {
					results.add(scriptInvocation);
					return true;
				}
				return false;
			}
		});
		lock.unlockWrite();
	}

	public void clearInteractiveScriptQueue() {
		interactiveScriptLock.lockWrite();
		interactiveScriptQueue.clear();
		interactiveScriptLock.unlockWrite();
	}

	public void clearNonInteractiveScriptQueue() {
		lock.lockWrite();
		internalQueue.clear();
		lock.unlockWrite();
	}

	public void clearInteractiveScriptQueue(List<ScriptInvocation> results) {
		interactiveScriptLock.lockWrite();
		results.addAll(interactiveScriptQueue);
		interactiveScriptQueue.clear();
		interactiveScriptLock.unlockWrite();
	}

	public void clearNonInteractiveScriptQueue(List<ScriptInvocation> results) {
		lock.lockWrite();
		results.addAll(internalQueue);
		internalQueue.clear();
		lock.unlockWrite();
	}

	public void clearInteractiveScriptStatus() {
		interactiveScriptLock.lockWrite();
		interactiveScriptRunning.set(false);
		interactiveScriptLock.unlockWrite();
	}

	public boolean isInteractiveScriptRunnung() {
		return interactiveScriptRunning.get();
	}

	public int getInteractiveScriptsQueued() {
		interactiveScriptLock.lockRead();
		final int result = interactiveScriptQueue.size();
		interactiveScriptLock.unlockRead();
		return result;
	}

	public int getNonInteractiveScriptsQueued() {
		return super.size();
	}
}


<|start_filename|>kotlin/src/main/java/org/mini2Dx/miniscript/kotlin/KotlinScriptExecutorPool.java<|end_filename|>
/**
 * The MIT License (MIT)
 * 
 * Copyright (c) 2017 <NAME>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.mini2Dx.miniscript.kotlin;

import java.util.Map;
import java.util.concurrent.BlockingQueue;

import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineBase.CompiledKotlinScript;
import org.mini2Dx.miniscript.core.*;
import org.mini2Dx.miniscript.core.exception.InsufficientCompilersException;
import org.mini2Dx.miniscript.core.exception.NoSuchScriptException;
import org.mini2Dx.miniscript.core.exception.ScriptExecutorUnavailableException;
import org.mini2Dx.miniscript.core.util.ReadWriteBlockingQueue;
import org.mini2Dx.miniscript.core.util.ReadWriteMap;

/**
 * An implementation of {@link ScriptExecutorPool} for Kotlin-based scripts
 */
public class KotlinScriptExecutorPool implements ScriptExecutorPool<CompiledKotlinScript> {
	private final Map<Integer, GameScript<CompiledKotlinScript>> scripts = new ReadWriteMap<>();
	private final Map<String, Integer> filepathToScriptId = new ReadWriteMap<String, Integer>();
	private final Map<Integer, String> scriptIdToFilepath = new ReadWriteMap<Integer, String>();
	private final BlockingQueue<ScriptExecutor<CompiledKotlinScript>> executors;
	private final GameScriptingEngine gameScriptingEngine;
	private final SynchronizedObjectPool<KotlinEmbeddedScriptInvoker> embeddedScriptInvokerPool = new SynchronizedObjectPool<KotlinEmbeddedScriptInvoker>() {
		@Override
		protected KotlinEmbeddedScriptInvoker construct() {
			return new KotlinEmbeddedScriptInvoker(gameScriptingEngine, KotlinScriptExecutorPool.this);
		}
	};

	public KotlinScriptExecutorPool(GameScriptingEngine gameScriptingEngine, int poolSize) {
		this.gameScriptingEngine = gameScriptingEngine;
		executors = new ReadWriteBlockingQueue<>(poolSize);

		for (int i = 0; i < poolSize; i++) {
			executors.offer(new KotlinScriptExecutor(this));
		}
	}

	@Override
	public void release(ScriptExecutor<CompiledKotlinScript> executor) {
		try {
			executors.put(executor);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	@Override
	public int getCompiledScriptId(String filepath) {
		return filepathToScriptId.getOrDefault(filepath, -1);
	}

	@Override
	public String getCompiledScriptPath(int scriptId) {
		return scriptIdToFilepath.get(scriptId);
	}

	GameScript<CompiledKotlinScript> getScript(int id) {
		return scripts.get(id);
	}

	@Override
	public int preCompileScript(String filepath, String scriptContent) throws InsufficientCompilersException {
		ScriptExecutor<CompiledKotlinScript> executor = executors.poll();
		if (executor == null) {
			throw new InsufficientCompilersException();
		}
		GameScript<CompiledKotlinScript> script = executor.compile(scriptContent);
		executor.release();
		scripts.put(script.getId(), script);
		filepathToScriptId.put(filepath, script.getId());
		scriptIdToFilepath.put(script.getId(), filepath);
		return script.getId();
	}

	@Override
	public ScriptExecutionTask<?> execute(int taskId, int scriptId, ScriptBindings scriptBindings,
			ScriptInvocationListener invocationListener, boolean syncCall) {
		ScriptExecutor<CompiledKotlinScript> executor = allocateExecutor();
		if (executor == null) {
			throw new ScriptExecutorUnavailableException(scriptId);
		}
		if(!scripts.containsKey(scriptId)) {
			executor.release();
			throw new NoSuchScriptException(scriptId);
		}
		return new ScriptExecutionTask<CompiledKotlinScript>(taskId, gameScriptingEngine, executor,
				scriptId, scripts.get(scriptId), scriptBindings, invocationListener, syncCall);
	}

	private ScriptExecutor<CompiledKotlinScript> allocateExecutor() {
		try {
			return executors.take();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public GameScriptingEngine getGameScriptingEngine() {
		return gameScriptingEngine;
	}

	public SynchronizedObjectPool<KotlinEmbeddedScriptInvoker> getEmbeddedScriptInvokerPool() {
		return embeddedScriptInvokerPool;
	}
}


<|start_filename|>lua/src/test/resources/invokeWithinScript.lua<|end_filename|>
scripts:invokeSync("default.lua")
intValue2 = 102