Is it possible to access the image file of a GamePiece via custom code (and also manipulate that image)?
Something like:
p is a variable of type GamePiece
Image i = (Image) p.getProperty(“Image”); (this does not actually work)
BufferedImage bi = new BufferedImage(i.getWidth(), i.getHeight(), BufferedImage.TYPE_INT_ARGB);
Can I get the image and then can I use the BufferedImage bi to change color pixel by pixel?
Is it possible to access the image file of a GamePiece via custom code
(and also manipulate that image)?
Something like:
p is a variable of type GamePiece
Image i = (Image) p.getProperty(“Image”); (this does not actually work)
BufferedImage bi = new BufferedImage(i.getWidth(), i.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Can I get the image and then can I use the BufferedImage bi to change
color pixel by pixel?
You can get the piece image via BasicPiece. However, you MUST NOT
alter the image your get that way. You have to make a copy of the
image and modify that. Modifying the image directly will interefere
with image caching.
Thanks for your earlier answer on this. I am coming back to it now as I work on our draggable overlays in VASL. I cannot find a way to get from GamePiece p to BasicPiece. I have tried setting properties. I have scanned the properties of p when running in my IDE. I cannot see how access BasicPiece in code.
If you have GamePiece p then it may either be a BasicPiece or equally likely a Decorator (Trait) that’s attached outside of one. It could also esoterically be a Stack or Deck, but those you’d probably ignore in your context.
So you’d need an algorithm like…
while (p instanceof Decorator) {
p = ((Decorator)p).getInner(); // Traverse inwards toward BasicPiece
}
if (p instanceof BasicPiece) { // Make sure we didn't start with a Stack or Deck (or null)
final BasicPiece bp = (BasicPiece)p;
// Now do whatever you want with the basic piece
}
This is assuming that the image you’re looking for is actually IN the BasicPiece (as opposed to being in a Layer trait a.k.a. the Embellishment class, which would be one of the decorators along the way). If you have an unknown GamePiece and you need to get to the outermost Decorator to traverse inward, then it’s something like:
p = Decorator.getOutermost(p); // Start all the way at outside
while (p instanceof Decorator) {
if (p instanceof Embellishment) {
// It's a "Layer" trait, so it might contain the actual image of the piece
}
p = ((Decorator)p).getInner(); // Traverse inwards toward BasicPiece
}
// Possibly check for BasicPiece image if didn't find one in an embellishment
I’m mentioning the piece image just because of your original question at the top of the thread. So if you’re looking for BasicPiece (or something along the way) for some other reason now then you can ignore the specifics about images but the general structure will be the same.
Hope this helps,
Brian
p.s. I replied to your message about the Chatter, in case you haven’t seen it.
You can get the piece image via BasicPiece. However, you MUST NOT
alter the image your get that way. You have to make a copy of the
image and modify that. Modifying the image directly will interefere
with image caching.
I am coming back to this issue, unfortunately. I thought I had found a way to not have to access the image file in code but that is no longer holding. So, I would appreciate your further help on this.
Context: in VASL we use counters to hold image files representing terrain features that can be dragged onto the map to change the map’s terrain. We also use other methods to do similar transformations. In one method we use various colour-to-colour mappings to alter a map image by changing the colour of individual pixels. This is done by the user while VASL is running. We would like to apply the same colour-to-colour alteration to the images used in our “terrain” counters.
Using your previous advice, I am now able to identify the specific image file being used by a “terrain” counter. My plan had been to grab create a BufferedImage from that file and then use our colour mapping code to update the pixel colours where required.
However, I note the advice, quoted above, NOT to alter a piece image directly, specifically “You have to make a copy of the image and modify that.”
Could I get a bit more advice on the mechanics of making a copy and modifying that image and then replacing the image being used by the counter? Is creating a BufferedImage from the piece image equivalent to “making a copy”? If so, and having then altered the BufferedImage, how do I write it back to the counter?
Is this the right approach? Is there a better way? We could achieve the desired result using layers within the “terrain” counters. However, there are reasons why we would prefer not to do so.
If you need a copy of a BufferedImage, you could do something like this:
final BufferedImage dst = new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
final Graphics2D g = dst.createGraphics();
g.drawImage(src, 0, 0, null);
g.dispose();
However, depending on what exactly you’re doing, you might want to do all of this inside an ImageOp so that the images you’re producing are in the image cache. It’s hard to be more specific without seeing the surrounding code for context.
BTW, we could use some help with converting pages in the current module library to the new one which will replace it.
Just thinking out loud here, so maybe totally off the mark.
Couldn’t the terrain tiles (encoded as pieces) have a Layer trait that holds all possible terrains? The user can then select the terrain from the terrain tile (piece) context menu.
You could have many such Layer traits for different “layers” on each piece - e.g.,
Base terrain (clear, woods, swamp, rough, hill, …)
If you want to be able to drag terrain, you could have the board execute BeanShell code when tile pieces are drop, which then changes the layer(s) of the on-board piece.
If you need to only be able to change terrain under certain circumstances, you can add RestrictCommand traits to the terrain pieces.
In this way, you do not need to do custom Java code.
Anyways, as I said, perhaps totally off the mark.
Without knowing exactly how VASSAL tiles and caches images, I think what you need to do is something like (the following code is not tested)
public static BasicPiece getInnermost(GamePiece p) {
while (p instanceof Decorator) {
p = ((Decorator) p).piece;
}
return p instanceof BasicPiece ? (BasicPiece)p : null;
}
public static BufferedImage getImage(BasicPiece p)
{
if (p == null) return null;
final GamePieceOpImpl op = new GamePieceOpImpl(p);
return op.eval();
}
Note that BasicPiece does not contain a reference to an image, only an ScaledImagePainter, which you can get a rendered copy via GamePieceOpImpl, then do your manipulations on that image, and write a new image to somewhere and then set the type with the new image filename on the BasicPiece.
Manipulate the image - the arguments ... is what will do the manipulation (unspecified here)
public static BufferedImage manipulate(BufferedImage src, ...)
{
if (src == null) return null;
// Work on src
return src; // This is a rendered copy of the image!
}
Save the manipulate image to a temporary file (e.g., /tmp/VASSALXXYYZZ.png)
public static String saveImage(BufferedImage img)
{
if (img == null) return "";
final File tmp = File.createTempFile("VASSAL",".png");
ImageIO.write(img, "jpg", tmp);
return tmp.getAbsolutePath();
}
Update the piece type with the new image name
public void updatePiece(BasicPiece p, String imageName)
{
if (imageName == "") return;
String type = p.getType();
final SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';');
st.nextToken();
char cloneKey = st.nextChar('\0');
char deleteKey = st.nextChar('\0');
String oldImageName = st.nextToken();
String commonName = st.nextToken();
final SequenceEncoder se =
new SequenceEncoder(cloneKey > 0 ? String.valueOf(cloneKey) : "", ';');
final String newType BasicPiece.ID +
se.append(deleteKey > 0 ? String.valueOf(deleteKey) : "")
.append(imageName)
.append(commonName).getValue()
p.mySetType(newType)
}
Do everything on a game piece with manipulations ... (unspecified here)
Note that this will only last for the current session most likely, because the written file is temporary. So when you read in a save or log, the terrain piece will specify a non-existing (most likely) image file (say /tmp/VASSALXXYYZZ.png), so it may not really be a good solution. If one can write the new image file to the module archive, then that would solve it, but then there might be a discrepancy between saves from different clients.
With respect to your first point, yes I agree that we could do this via layers and avoid custom code completely. In fact, there are instances in which we do exactly that. However, there are a number of situations and transformations where we don’t want to use layers and are looking for a different solution.
With respect to your detailed code suggestions, they seem clear and logical to me. I will give them a whirl. Do you think that they conform to @uckelman’s advice to “do all of this inside an ImageOp so that the images you’re producing are in the image cache”?
you might want to do all of this inside an ImageOp so that the images you’re producing are in the image cache. It’s hard to be more specific without seeing the surrounding code for context.
We want to do this within the method ASLBoard.setTerrain(). This code is invoked in 3 ways:
when a new game is created
when a saved game is opened
when an open game is changed by using the Pick New Boards For This Scenario menu option (which enables a range of changes to the game not just different boards).
Here is the code for setTerrain
//
// Add draggable overlays from overlay extension
public void setTerrain(String changes) throws BoardException {
terrainChanges = changes;
terrain = null;
if (changes == null) {
return;
}
for (int i = 0; i < overlays.size(); ++i) {
if (overlays.get(i) instanceof SSROverlay) {
overlays.remove(i--);
}
}
if (changes.length() > 0) {
terrain = new SSRFilter(changes, boardFile, this);
for (SSROverlay o : terrain.getOverlays()) {
overlays.add(0, o);
}
}
// enabling terran transforms for draggable overlays from the overlays extension
// get the set of new draggable overlays
LinkedList<GamePiece> draggableOverlays = new LinkedList<GamePiece>();
if (map != null) {
GamePiece draggableOverlay;
GamePiece[] p = map.getPieces();
for (GamePiece aP : p) {
if (aP instanceof Stack) {
for (PieceIterator pi = new PieceIterator(((Stack) aP).getPiecesIterator()); pi.hasMoreElements(); ) {
GamePiece p2 = pi.nextPiece();
if (p2.getProperty("overlay") != null) {
draggableOverlays.add(p2);
}
}
} else {
if (aP.getProperty("overlay") != null) {
draggableOverlays.add(aP);
}
}
}
}
resetImage();
if (draggableOverlays.size() > 0) {
for (GamePiece p : draggableOverlays){
String imageNames = p.getLocalizedName();
while (p instanceof Decorator) {
p = ((Decorator)p).getInner(); // Traverse inwards toward BasicPiece
}
if (p instanceof BasicPiece) { // Make sure we didn't start with a Stack or Deck (or null)
final BasicPiece bp = (BasicPiece) p;
HashMap Names = (HashMap) bp.getPublicProperty("snapshot");
String imagename = (String) Names.get("_Image");
// ToDo
// 1. get the image as a buffered image
// 2. manipulate the Buffered image - resetImage may work (with changes?)
// 3. replace the original image with the changed Buffered image
}
}
}
}
I need the HashMap to obtain the name of the image file currently being displayed as the terrain Pieces often contain layers and this was the only way I could find to determine which layer was being displayed.
The method resetImage() currently performs the same manipulation of the board/overlay images as we would like to perform on the image in the draggable overlay terrain Piece. So I am hopeful that with minor changes it can be used to create the changed BufferedImage.
protected void resetImage() {
final ImageTileSource ts =
GameModule.getGameModule().getImageTileSource();
boolean tiled = false;
try {
tiled = ts.tileExists(imageFile, 0, 0, 1.0);
} catch (ImageIOException e) {
// ignore, not tiled
}
if (tiled) {
FileArchive fa = null;
try {
fa = new ZipArchive(boardFile);
} catch (IOException e) {
e.printStackTrace();
}
baseImageOp = new SourceOpTiledBitmapImpl(imageFile, fa);
} else {
baseImageOp = Op.load(imageFile);
}
boardImageOp = new BoardOp();
uncroppedSize = baseImageOp.getSize();
fixedBoundaries = false;
scaledImageOp = null;
}
Thanks again for your continued help.
With respect to your BTW, I am more than happy to help and will proceed as per the link.
OK, so you have call-backs which may solve some of the issues raised at the end of my last message.
First, I would refactor this into smaller pieces of code a la
Reset the overlays
protected void resetOverlays(String changes) {
for (int i = 0; i < overlays.size(); ++i) {
if (overlays.get(i) instanceof SSROverlay) {
overlays.remove(i--);
}
}
if (changes.length() > 0) {
terrain = new SSRFilter(changes, boardFile, this);
for (SSROverlay o : terrain.getOverlays()) {
overlays.add(0, o);
}
}
}
Populate overlays from the pieces on the map
protected LinkedList<GamePiece> populateOverlays(Map map) {
if (map == null) return null;
LinkedList<GamePiece> draggableOverlays = new LinkedList<GamePiece>();
for (GamePiece aP : map.getPieces()) {
if (aP instanceof Stack) {
for (PieceIterator pi = new PieceIterator(((Stack) aP).getPiecesIterator()); pi.hasMoreElements(); ) {
GamePiece p2 = pi.nextPiece();
if (p2.getProperty("overlay") != null) {
draggableOverlays.add(p2);
}
}
} else {
if (aP.getProperty("overlay") != null) {
draggableOverlays.add(aP);
}
}
}
return draggableOverlays;
}
Get the BasicPiece of a GamePiece.
public static BasicPiece getInnermost(GamePiece p) {
while (p instanceof Decorator) {
p = ((Decorator) p).piece;
}
return p instanceof BasicPiece ? (BasicPiece)p : null;
}
Get the image of a GamePiece. This uses ImageOp (rather the derived GamePieceOp) to render the image into a buffered image, so …
… I believe “yes”.
public static BufferedImage getImage(GamePiece p)
{
if (p == null) return null;
final GamePieceOpImpl op = new GamePieceOpImpl(p);
return op.eval();
}
protected void processOverlays(LinkedList<GamePiece> overlays) {
if (overlays == null) return;
for (GamePiece p : overlays) {
BasicPiece bp = getInnermost(p);
processOverlay(bp)
}
}
protected void procesOverlay(GamePiece bp) {
BufferedImage img = getImage(bp);
// Manipulate the image as needed here. Then, afterwards it needs to be written to disk somehow.
}
If you just want the image (not necessarily the image file name), you can skip finding the inner most piece (i.e., the BasicPiece) in processOverlays and just call processOverlay on each GamePiece. That should render the current image into the returned buffered image.
Something seems to be missing in the above. What is imageFile, boardFile, and do you use boardImageOp?
It seems like you are adding a tile source which will read from some auxiliary ZIP file boardFile. If that’s the case, then you can add your manipulated image to that ZIP archive. But I’m not entirely sure that’s what you do, and that would also mean that users will need to share the auxiliary ZIP archive. Note that Op and ImageOp does not have methods to write images - only to load them. Of course, if you add your manipulated image to the cached images through a tile source, then you need to add the manipulated image to the auxiliary ZIP archive only for the next session, and you can use standard methods then. That is, when you build the modified image, you immediately put it into the cache and save it to the auxiliary ZIP file. Then, at the start of the next session, the auxiliary ZIP archive must be added as a tile source, and the manipulated image can be read from there.
Note, I believe you still need to manipulate the BasicPiece (or some layer) to refer to the manipulated image file name with the getType and mySetType methods.
No, the above is not what I was suggesting w/r/t wrapping the image modification in an ImageOp and will not result in your modified images being cached. I don’t follow any of what was said about saving images or why the code checks the tile cache; I don’t understand why you’d need to do either of those things. Piece images are never in the tile cache.
What I meant was that you should make a class which implements ImageOp in which the transformation occurs, and then you should get the BufferedImage you need from it.
While I keep getting distracted from the original intent of this question, I do keep coming back to your responses. Just today, I was able to use some of Christian’s advice to solve a separate but similar challenge.
Thanks to all the folks who help with VASSAL. You are very supportive of module developers.