Skip to content

Commit

Permalink
Merge pull request #347 from vincent4vx/feature-ai-scripting
Browse files Browse the repository at this point in the history
feat(ai): Add scripting for AI + refactor scripting system with admin
  • Loading branch information
vincent4vx authored May 20, 2024
2 parents e2cf542 + 2dbb4c6 commit ab7ab10
Show file tree
Hide file tree
Showing 72 changed files with 2,649 additions and 321 deletions.
69 changes: 69 additions & 0 deletions scripts/ai/Random.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import fr.quatrevieux.araknemu.game.fight.ai.action.builder.GeneratorBuilder
import fr.quatrevieux.araknemu.game.fight.ai.action.util.CastSpell
import fr.quatrevieux.araknemu.game.fight.ai.action.util.Movement
import fr.quatrevieux.araknemu.game.fight.ai.factory.AbstractAiBuilderFactory
import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation
import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator

/*
* This file is part of Araknemu.
*
* Araknemu is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Araknemu is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Araknemu. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2017-2024 Vincent Quatrevieux
*/

/**
* Example of a random AI
*
* To create a new AI type you have to extends AbstractAiBuilderFactory and override one of the configure method
* The AI name will be the class name in upper case. If you want to change the name, you can override the name() method
*
* Note: be aware of the maximum length of the name. By default the maximum length is 12 characters.
*/
class Random extends AbstractAiBuilderFactory {
final Simulator simulator
final java.util.Random random

// The scripting API will inject dynamically all the required dependencies
// If one of the dependencies is not declared in the container, it will be created automatically
Random(Simulator simulator) {
this.simulator = simulator
this.random = new java.util.Random()
}


// Override this method to configure the AI
// Note: you can also override configure(GeneratorBuilder builder, PlayableFighter fighter) instead of this one to have access to the fighter
@Override
protected void configure(GeneratorBuilder builder) {
// Now you can add actions to the AI pipeline
// The pipeline defines the priority of the actions
// If the first action can be executed, it will be executed until it can't
// And then the second action will be executed, and so on
builder
.add(new Movement({ Math.random() }, { true }))
.add(new CastSpell(simulator, new CastSpell.SimulationSelector() {
@Override
boolean valid(CastSimulation simulation) {
random.nextBoolean()
}

@Override
double score(CastSimulation simulation) {
Math.random()
}
}))
}
}
7 changes: 7 additions & 0 deletions src/main/java/fr/quatrevieux/araknemu/core/di/Container.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,11 @@ public default <T> Container with(@NonNull T value, Class<? super T>... interfac
public default Container withAll(ScopedContainer.Mapping<?>... mappings) {
return ScopedContainer.fromMapping(this, mappings);
}

/**
* Create an instantiator from the container
*/
public default Instantiator instantiator() {
return new ContainerInstantiator(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* This file is part of Araknemu.
*
* Araknemu is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Araknemu is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Araknemu. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2017-2024 Vincent Quatrevieux
*/

package fr.quatrevieux.araknemu.core.di;

import org.checkerframework.checker.nullness.qual.Nullable;

import java.lang.reflect.Constructor;

/**
* Default implementation of {@link Instantiator} using a {@link Container} to resolve
* already defined services.
*
* When an object is not declared explicitly in the container, the instantiator will try to
* resolve recursively the constructor arguments and instantiate them.
*
* This implementation will not cache instances. Unless the type is declared in the container,
* a new instance is created each time.
*/
public final class ContainerInstantiator implements Instantiator {
private final Container container;

public ContainerInstantiator(Container container) {
this.container = container;
}

@Override
@SuppressWarnings("argument")
public <T> T instantiate(Class<T> clazz) throws ContainerException {
if (container.has(clazz)) {
return container.get(clazz);
}

if (clazz.isPrimitive() || clazz.isArray() || clazz.getName().startsWith("java.")) {
throw new InstantiatorException("Cannot instantiate primitive types or classes from java.* package");
}

for (Constructor<?> constructor : clazz.getConstructors()) {
final @Nullable Object @Nullable [] parameters = resolveArguments(constructor);

if (parameters == null) {
continue;
}

try {
return (T) constructor.newInstance(parameters);
} catch (Exception e) {
// Ignore and try next constructor
}
}

// No constructor found : try to instantiate without constructor
try {
return clazz.newInstance();
} catch (Exception e) {
throw new InstantiatorException("Cannot instantiate object of type " + clazz.getName(), e);
}

}

/**
* Try to resolve constructor arguments
* When an argument value can't be resolved, return null to indicate that the constructor can't be used
*
* @return Array of arguments or null when cannot instantiate the object
*/
private @Nullable Object @Nullable [] resolveArguments(Constructor<?> constructor) {
final Class<?>[] parametersTypes = constructor.getParameterTypes();
final @Nullable Object[] parameters = new Object[parametersTypes.length];

for (int i = 0; i < parameters.length; ++i) {
try {
parameters[i] = instantiate(parametersTypes[i]);
} catch (ContainerException e) {
return null;
}
}

return parameters;
}
}
36 changes: 36 additions & 0 deletions src/main/java/fr/quatrevieux/araknemu/core/di/Instantiator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* This file is part of Araknemu.
*
* Araknemu is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Araknemu is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Araknemu. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2017-2024 Vincent Quatrevieux
*/

package fr.quatrevieux.araknemu.core.di;

/**
* Base type for instantiate objects using autowiring
* Use to instantiate objects declared externally, like using a script engine
*/
public interface Instantiator {
/**
* Try to instantiate an object of the given class
*
* @param clazz The class to instantiate
* @return The instantiated object
*
* @param <T> The type of the object
*/
public <T> T instantiate(Class<T> clazz) throws ContainerException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* This file is part of Araknemu.
*
* Araknemu is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Araknemu is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Araknemu. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2017-2024 Vincent Quatrevieux
*/

package fr.quatrevieux.araknemu.core.di;

/**
* Exception thrown when an error occurs during object instantiation
*/
public class InstantiatorException extends ContainerException {
public InstantiatorException(String message) {
super(message);
}

public InstantiatorException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void dispatch(Object event) {
for (Listener listener : container) {
try {
listener.on(event);
} catch (RuntimeException e) {
} catch (Exception e) {
logger.error("Error during execution of listener " + listener.getClass().getName(), e);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* This file is part of Araknemu.
*
* Araknemu is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Araknemu is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Araknemu. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2017-2024 Vincent Quatrevieux
*/

package fr.quatrevieux.araknemu.core.scripting;

import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;

/**
* Handle proxy creation to enable hot reload of scripts
*
* @param <T> The type of the script class
*/
public final class HotReloadProxy<@NonNull T> implements InvocationHandler, HotReloadableScript<T> {
private final ScriptLoader loader;
private final Logger logger;
private final Class<T> type;
private final Path file;
private @NonNull T inner;
private FileTime lastModified;

HotReloadProxy(ScriptLoader loader, Class<T> type, Logger logger, Path file, @NonNull T inner) throws IOException {
this.loader = loader;
this.logger = logger;
this.type = type;
this.file = file;
this.inner = inner;
this.lastModified = Files.getLastModifiedTime(file);
}

@Override
@SuppressWarnings("override.return")
public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass().isAssignableFrom(HotReloadableScript.class)) {
return method.invoke(this, args);
}

return method.invoke(load(), args);
}

@Override
public T getInternalInstance() {
return inner;
}

@Override
public Path getScriptFile() {
return file;
}

@Override
public Class<T> getScriptType() {
return type;
}

/**
* Create the proxy instance
*/
public @NonNull T instantiate() {
return type.cast(Proxy.newProxyInstance(
type.getClassLoader(),
new Class[] {
type,
HotReloadableScript.class,
},
this
));
}

private @NonNull T load() {
try {
final FileTime newModified = Files.getLastModifiedTime(file);

if (!newModified.equals(lastModified)) {
logger.debug("The script {} has been modified. Reload it.", file);

final T newInner = loader.load(file, type);

if (newInner != null) {
inner = newInner;
lastModified = newModified;
} else {
logger.error("The new version of the script {} is not compatible with type {}. Keep last version.", file, type);
}
}
} catch (Throwable e) {
logger.error("Failed to reload script " + file + ". Keep last version.", e);
}

return inner;
}
}
Loading

0 comments on commit ab7ab10

Please sign in to comment.