-
-
Notifications
You must be signed in to change notification settings - Fork 308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tilemap editor plugins #7665
Tilemap editor plugins #7665
Changes from 8 commits
970c450
73f2399
477e3f0
abf172a
c87af1a
52d5b0b
5a9c341
cf4b17e
9fe80e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.defold.extension.editor; | ||
|
||
import java.util.List; | ||
|
||
import com.dynamo.gamesys.proto.Tile.TileCell; | ||
|
||
import com.defold.extension.editor.TilemapPlugins.TilemapLayer; | ||
|
||
public interface ITilemapPlugin { | ||
|
||
public List<TileCell> onClearTile(int x, int y, TilemapLayer layer); | ||
public List<TileCell> onPaintTile(int x, int y, TilemapLayer layer); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package com.defold.extension.editor; | ||
|
||
import java.lang.reflect.Field; | ||
|
||
import java.util.List; | ||
import java.util.ArrayList; | ||
import java.util.Map; | ||
import java.util.HashMap; | ||
import com.dynamo.bob.plugin.PluginScanner; | ||
import com.dynamo.bob.fs.DefaultFileSystem; | ||
import com.dynamo.bob.Project; | ||
|
||
import com.dynamo.bob.ClassLoaderScanner; | ||
import com.dynamo.gamesys.proto.Tile.TileCell; | ||
|
||
|
||
public class TilemapPlugins { | ||
|
||
/** | ||
* Wrapper for the cells of a tilemap layer. Each cell is | ||
* keyed on an index created from the x and y position of | ||
* the cell. | ||
*/ | ||
public static class TilemapLayer { | ||
|
||
private Map<Long, Object> cells; | ||
private String layerId; | ||
|
||
public TilemapLayer(Map<Long, Object> cells, String layerId) { | ||
this.cells = cells; | ||
this.layerId = layerId; | ||
} | ||
|
||
public String getLayerId() { | ||
return layerId; | ||
} | ||
|
||
public long xyToIndex(int x, int y) { | ||
// 4294967295L = 0xFFFFFFFF | ||
// using 0xFFFFFFFF messes up the & operation for some reason | ||
return (y << Integer.SIZE) | (x & 4294967295L); | ||
} | ||
|
||
public Integer get(int x, int y) { | ||
Object cell = cells.get(xyToIndex(x, y)); | ||
if (cell == null) { | ||
return null; | ||
} | ||
try { | ||
// It would be better if the (defrecord Tile) in tile_map.clj | ||
// implemented a shared interface so that we don't have to use | ||
// reflection to get the field(s) of the tile | ||
Class<?> cls = cell.getClass(); | ||
Field field = cls.getField("tile"); | ||
Long value = (Long)field.get(cell); | ||
return new Integer(value.intValue()); | ||
} | ||
catch (Exception e) { | ||
System.err.println("Unable to get field: " + e.getMessage()); | ||
} | ||
return null; | ||
} | ||
} | ||
|
||
|
||
private static List<ITilemapPlugin> tilemapPlugins = null; | ||
|
||
public static void init(ClassLoader loader) { | ||
if (tilemapPlugins != null) { | ||
return; | ||
} | ||
ClassLoaderScanner scanner = new ClassLoaderScanner(loader); | ||
try { | ||
tilemapPlugins = PluginScanner.getOrCreatePlugins(scanner, "com.defold.extension.editor", ITilemapPlugin.class); | ||
} | ||
catch(Exception e) { | ||
System.err.println("Exception while scanning for tilemap plugins"); | ||
tilemapPlugins = new ArrayList<>(); | ||
} | ||
} | ||
|
||
public static List<TileCell> onClearTile(int x, int y, Map<Long, Object> cells, String layerId) { | ||
List<TileCell> changedCells = new ArrayList<>(); | ||
if (tilemapPlugins != null) { | ||
TilemapLayer layer = new TilemapLayer(cells, layerId); | ||
for (ITilemapPlugin plugin : tilemapPlugins) { | ||
changedCells.addAll(plugin.onClearTile(x, y, layer)); | ||
} | ||
} | ||
return changedCells; | ||
} | ||
public static List<TileCell> onPaintTile(int x, int y, Map<Long, Object> cells, String layerId) { | ||
List<TileCell> changedCells = new ArrayList<>(); | ||
if (tilemapPlugins != null) { | ||
TilemapLayer layer = new TilemapLayer(cells, layerId); | ||
for (ITilemapPlugin plugin : tilemapPlugins) { | ||
changedCells.addAll(plugin.onPaintTile(x, y, layer)); | ||
} | ||
} | ||
return changedCells; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,6 @@ | |
import java.util.HashMap; | ||
import java.lang.reflect.Modifier; | ||
|
||
import com.dynamo.bob.Bob; | ||
import com.dynamo.bob.Project; | ||
import com.dynamo.bob.IClassScanner; | ||
import com.dynamo.bob.CompileExceptionError; | ||
|
@@ -33,6 +32,16 @@ public class PluginScanner { | |
|
||
private static HashMap<String, Object> pluginsCache = new HashMap<>(); | ||
|
||
|
||
public static <T> T getOrCreatePlugin(String packageName, Class<T> pluginBaseClass) throws CompileExceptionError { | ||
IClassScanner scanner = Project.getClassLoaderScanner(); | ||
if (scanner == null) { | ||
logger.info("PluginScanner has no class loader scanner"); | ||
return null; | ||
} | ||
return getOrCreatePlugin(scanner, packageName, pluginBaseClass); | ||
} | ||
|
||
/** | ||
* Get a previously cached instance or find and create an instance of a class | ||
* extending a specific base class and located within a specific package. The | ||
|
@@ -45,13 +54,14 @@ public class PluginScanner { | |
* This function will generate a CompileExceptionError if more than one | ||
* class in the package path extends the same base class. | ||
* | ||
* @param scanner | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to be able to pass classloader scanner from the editor |
||
* @param packageName | ||
* @param pluginBaseClass | ||
* @return Class instance or null if no class was found | ||
*/ | ||
public static <T> T getOrCreatePlugin(String packageName, Class<T> pluginBaseClass) throws CompileExceptionError { | ||
public static <T> T getOrCreatePlugin(IClassScanner scanner, String packageName, Class<T> pluginBaseClass) throws CompileExceptionError { | ||
|
||
List<T> plugins = getOrCreatePlugins(packageName, pluginBaseClass); | ||
List<T> plugins = getOrCreatePlugins(scanner, packageName, pluginBaseClass); | ||
T plugin = null; | ||
if (plugins != null) { | ||
if (plugins.size() > 1) { | ||
|
@@ -62,6 +72,16 @@ public static <T> T getOrCreatePlugin(String packageName, Class<T> pluginBaseCla | |
} | ||
return plugin; | ||
} | ||
|
||
|
||
public static <T> List<T> getOrCreatePlugins(String packageName, Class<T> pluginBaseClass) throws CompileExceptionError { | ||
IClassScanner scanner = Project.getClassLoaderScanner(); | ||
if (scanner == null) { | ||
logger.info("PluginScanner has no class loader scanner"); | ||
return null; | ||
} | ||
return getOrCreatePlugins(scanner, packageName, pluginBaseClass); | ||
} | ||
|
||
/** | ||
* Get a previously cached instances or find and create instances of classes | ||
|
@@ -72,11 +92,12 @@ public static <T> T getOrCreatePlugin(String packageName, Class<T> pluginBaseCla | |
* - Not be abstract | ||
* - Extend the base class | ||
* | ||
* @param scanner | ||
* @param packageName | ||
* @param pluginBaseClass | ||
* @return List with class instances or null if no class was found | ||
*/ | ||
public static <T> List<T> getOrCreatePlugins(String packageName, Class<T> pluginBaseClass) throws CompileExceptionError { | ||
public static <T> List<T> getOrCreatePlugins(IClassScanner scanner, String packageName, Class<T> pluginBaseClass) throws CompileExceptionError { | ||
|
||
// check if we've already searched for and cached a plugin for this package path and base class | ||
// and if that is the case return the cached instance | ||
|
@@ -92,12 +113,6 @@ public static <T> List<T> getOrCreatePlugins(String packageName, Class<T> plugin | |
return plugins; | ||
} | ||
|
||
IClassScanner scanner = Project.getClassLoaderScanner(); | ||
if (scanner == null) { | ||
logger.info("PluginScanner has no class loader scanner"); | ||
return null; | ||
} | ||
|
||
logger.info("PluginScanner searching %s for base class %s", packageName, pluginBaseClass); | ||
|
||
List<T> plugins = new ArrayList<>(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,7 +40,8 @@ | |
[editor.tile-source :as tile-source] | ||
[editor.validation :as validation] | ||
[editor.workspace :as workspace]) | ||
(:import [com.dynamo.gamesys.proto Tile$TileGrid Tile$TileGrid$BlendMode Tile$TileLayer] | ||
(:import [com.dynamo.gamesys.proto Tile$TileGrid Tile$TileGrid$BlendMode Tile$TileLayer Tile$TileCell] | ||
[com.defold.extension.editor TilemapPlugins] | ||
[com.jogamp.opengl GL2] | ||
[editor.gl.shader ShaderLifecycle] | ||
[javax.vecmath Matrix4d Point3d Vector3d])) | ||
|
@@ -84,21 +85,44 @@ | |
(bit-or (bit-shift-left y Integer/SIZE) | ||
(bit-and x 0xFFFFFFFF))) | ||
|
||
(defn update-cell-from-pb | ||
[cell-map ^Tile$TileCell cell] | ||
(assoc! cell-map | ||
(cell-index (.getX cell) (.getY cell)) | ||
(->Tile (.getX cell) | ||
(.getY cell) | ||
(.getTile cell) | ||
(if (= (.getHFlip cell) 1) true false) | ||
(if (= (.getVFlip cell) 1) true false) | ||
(if (= (.getRotate90 cell) 1) true false)))) | ||
|
||
(defn paint-cell! | ||
[cell-map x y tile h-flip v-flip rotate90] | ||
(if tile | ||
(assoc! cell-map (cell-index x y) (->Tile x y tile h-flip v-flip rotate90)) | ||
(dissoc! cell-map (cell-index x y)))) | ||
[cell-map layer-id x y tile h-flip v-flip rotate90] | ||
; figure out where to best do this once | ||
(TilemapPlugins/init workspace/class-loader) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initialising every frame is ofc not necessary. The question is where to put this line of code? |
||
; update the cell-map | ||
; send to plugins | ||
; apply changes from plugins to cell-map | ||
(let [updated-cell-map (if tile | ||
(assoc! cell-map (cell-index x y) (->Tile x y tile h-flip v-flip rotate90)) | ||
(dissoc! cell-map (cell-index x y))) | ||
updated-cells-pb (if tile | ||
(TilemapPlugins/onPaintTile x y (persistent! updated-cell-map) layer-id) | ||
(TilemapPlugins/onClearTile x y (persistent! updated-cell-map) layer-id))] | ||
(reduce (fn [map cell] | ||
(update-cell-from-pb map cell)) | ||
updated-cell-map | ||
updated-cells-pb))) | ||
|
||
(defn make-cell-map | ||
[cells] | ||
[cells layer-id] | ||
(persistent! (reduce (fn [ret {:keys [x y tile h-flip v-flip rotate90] :or {h-flip 0 v-flip 0} :as cell}] | ||
(paint-cell! ret x y tile (not= 0 h-flip) (not= 0 v-flip) (not= 0 rotate90))) | ||
(paint-cell! ret layer-id x y tile (not= 0 h-flip) (not= 0 v-flip) (not= 0 rotate90))) | ||
(transient (int-map/int-map)) | ||
cells))) | ||
|
||
(defn paint | ||
[cell-map [sx sy] brush] | ||
[cell-map layer-id [sx sy] brush] | ||
(let [{:keys [width height tiles]} brush] | ||
(let [ex (+ sx width) | ||
ey (+ sy height)] | ||
|
@@ -109,7 +133,7 @@ | |
(if (< y ey) | ||
(if (< x ex) | ||
(let [{:keys [tile h-flip v-flip rotate90]} (first tiles)] | ||
(recur (inc x) y (rest tiles) (paint-cell! cell-map x y tile h-flip v-flip rotate90))) | ||
(recur (inc x) y (rest tiles) (paint-cell! cell-map layer-id x y tile h-flip v-flip rotate90))) | ||
(recur sx (inc y) tiles cell-map)) | ||
(persistent! cell-map)))))) | ||
|
||
|
@@ -356,7 +380,7 @@ | |
[layer-node [LayerNode {:id (:id tile-layer) | ||
:z (:z tile-layer) | ||
:visible (not= 0 (:is-visible tile-layer)) | ||
:cell-map (make-cell-map (:cell tile-layer))}]] | ||
:cell-map (make-cell-map (:cell tile-layer) (:id tile-layer))}]] | ||
(attach-layer-node parent layer-node)))) | ||
|
||
|
||
|
@@ -967,11 +991,12 @@ | |
(when-let [active-layer (g/node-value self :active-layer evaluation-context)] | ||
(when-let [current-tile (g/node-value self :current-tile evaluation-context)] | ||
(let [brush (g/node-value self :brush evaluation-context) | ||
op-seq (gensym)] | ||
op-seq (gensym) | ||
layer-id (g/node-value active-layer :id evaluation-context)] | ||
(swap! state assoc :last-tile current-tile) | ||
[(g/set-property self :op-seq op-seq) | ||
(g/operation-sequence op-seq) | ||
(g/update-property active-layer :cell-map paint current-tile brush)])))) | ||
(g/update-property active-layer :cell-map paint layer-id current-tile brush)])))) | ||
|
||
(defmethod update-op :paint | ||
[op self action state evaluation-context] | ||
|
@@ -980,9 +1005,10 @@ | |
(when (not= current-tile (-> state deref :last-tile)) | ||
(swap! state assoc :last-tile current-tile) | ||
(let [brush (g/node-value self :brush evaluation-context) | ||
op-seq (g/node-value self :op-seq evaluation-context)] | ||
op-seq (g/node-value self :op-seq evaluation-context) | ||
layer-id (g/node-value active-layer :id evaluation-context)] | ||
[(g/operation-sequence op-seq) | ||
(g/update-property active-layer :cell-map paint current-tile brush)]))))) | ||
(g/update-property active-layer :cell-map paint layer-id current-tile brush)]))))) | ||
|
||
(defmethod end-op :paint | ||
[op self action state evaluation-context] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Questionable if the interface for editor plugins should be located here in bob?