Custom SnapTo

I have now managed to make a custom modification of VASSAL. What follows will describe what was done and also serve as a tutorial for other newbies.

VASSAL uses Java. You don’t have to have prior knowledge of Java but you obviously need some general programming experience (my background is in C# and .NET).

I used the Eclipse Java IDE. You need to add the library Vengine.jar which is found in the lib directory of the VASSAL installation. You also need the Java Developers Kit (JDK).

Problem: I wanted to modify the Wooden Ships & Iron Men module (an old AH game of naval warfare from the late 18th and early 19th centuries). The original designer did a good job, but I wanted to add the real map with shore outlines and make the counters (ships) snap to hexsides. Making the real map involved scanning in the original map and removing the grid with GIMP. When it comes to the hexside snapping, the problem is this: The counters (ships) are two hexes long. Depending on the facing of each counter, only two sides of every hex are legal snap-to points.

Solution: VASSAL is so designed that it dynamically loads classes at run time. This makes it easy to extend classes and add them to the buildFile.

After a while I found out that you need to extend three classes: Map, HexGrid and Board.

Here is the code for MyMap.java:

package MySnapTo;

import java.awt.Point;
import java.awt.Rectangle;

import VASSAL.build.module.Map;
import VASSAL.build.module.map.boardPicker.Board;
import VASSAL.counters.GamePiece;
import VASSAL.counters.Properties;
import VASSAL.counters.Stack;



public class MyMap extends Map {
	

	public MyMap() {
		
		super ();
	}
	
	
	public int findFacing() {
		 
		int facing = 1;
		GamePiece piece = null;
		
		GamePiece[] pieces = this.getPieces();
			
			
		for (int i = 0; i < pieces.length; i++)
		{
			
			piece = pieces[i];
			
			if (piece != null)
			{
				Object propertyObject = piece.getProperty(Properties.SELECTED);
				
				if (propertyObject != null)						
					if ((boolean)propertyObject == true)	
						break;
			}
		}
		
		
		if (piece != null)
		{	
			Object propertyObject =	piece.getProperty("_Facing");
			
			if (propertyObject != null)
				facing = Integer.parseInt(propertyObject.toString());
		}
		
	
		return facing;
	}
	
	
	// copy from Map except call to custom snapTo
	@Override
	public Point snapTo(Point p) {
		
		int facing = findFacing();
		
		
		Point snap = new Point(p);

	    final Board b = findBoard(p);
	    
	    if (b == null) return snap;
	    
	    // custom cast
	    final MyBoard mb = (MyBoard) b;

	    final Rectangle r = mb.bounds();
	    snap.translate(-r.x, -r.y);
	    
	    // call to custom snap
	    snap = mb.snapTo(snap, facing);
	    
	    
	    snap.translate(r.x, r.y);
	    
	    if (findBoard(snap) == null) {
	      snap.translate(-r.x, -r.y);
	      if (snap.x == r.width) {
	        snap.x = r.width - 1;
	      }
	      else if (snap.x == -1) {
	        snap.x = 0;
	      }
	      if (snap.y == r.height) {
	        snap.y = r.height - 1;
	      }
	      else if (snap.y == -1) {
	        snap.y = 0;
	      }
	      snap.translate(r.x, r.y);
	    }
	    
	    return snap;
	}
	

}

Here is the code for MyHexGrid.java:

package MySnapTo;

import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.List;

import static java.lang.Math.*;
import VASSAL.build.Buildable;
import VASSAL.build.GameModule;
import VASSAL.build.module.Chatter;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.map.PieceCollection;
import VASSAL.build.module.map.boardPicker.Board;
import VASSAL.build.module.map.boardPicker.board.HexGrid;
import VASSAL.build.module.map.boardPicker.board.mapgrid.GridNumbering;
import VASSAL.build.module.map.PieceMover;
import VASSAL.command.Command;
import VASSAL.counters.EditablePiece;
import VASSAL.counters.GamePiece;
import VASSAL.counters.PieceFinder.Movable;
import VASSAL.counters.Stack;
import VASSAL.tools.imports.adc2.ADC2Module.Piece;
import VASSAL.counters.*;



public class MyHexGrid extends HexGrid {	

	
	public MyHexGrid ()
	{
		super();

	}
	 
	 
	/*
		holds number of whole dx increments along x-axis
		is communicated from sideX to sideY
	 */
	private int nx = 0;
	 
	/*
		x-coordinate of closest snap point to input x
		facing runs clockwise from 1 to 6 with 1 north
		dx is the vertical length of two hexsides
	*/
	protected int sideX(int x, int facing) 
	{	 
		 double inc = dx;
		 int start = 0;
		  
		
		 if (facing == 1 || facing == 4)
			 start = 0;
		 else // facing 2,3,5,6
			 start = (int) (dx / 2);
		 
		 
		 nx =  (int)(floor(((x - origin.x) / dx) + 0.25));
		 
		 
		 return (int) (nx * inc + start + origin.x);
	}
	
	 	
	
	/*
	 	y-coordinate of closest snap point to input y
	 	facing runs clockwise from 1 to 6 with 1 north
		dy is the horizontal length of two hexsides
	*/
	protected int sideY(int y, int facing)
	{
		 
		double start;
		double inc;
		double interval;
		
		
		inc = dy;
		interval = dy / 2;
		
		
			 
		if (facing == 1 || facing == 4)
		{
			 if (nx % 2 == 0)
				 start = -(dy / 2);
			 else
				 start = 0;	
		}
		else if (facing == 2 || facing == 5)
		{
			 if (nx % 2 == 0)
				 start = -(dy / 4);
			 else
				 start =  dy / 4;
		}
		else // facing 3 and 6
		{
			 if (nx % 2 == 0)
				 start = dy / 4;
			 else
				 start = -(dy / 4);
		}
		
		 
		return (int) ( (int)(floor((y - origin.y - start + interval) / inc) * inc) + start + origin.y ); 
		
	}
	  
	 
	 
	 /*
	    snap to nearest hexside depending on counter facing
	 */
	 public Point snapToHexSide(Point p, int facing)
	 { 		
		 
	    int x = sideX(p.x, facing);
	    int y = sideY(p.y, facing);
		
	    Point result = new Point(x, y);
	    
	    return result;
	 }
	 
	 
	 /*
 		my SnapTo with facing
	  */
	 public Point snapTo(Point p, int facing) {
		 
		 // make sure we have a legal facing
		 if (facing < 1 || facing > 6)
			 facing = 1;
		 
		 Point n  = snapToHexSide(p, facing);
		 
		 return n;
	 }
	 

	
	public void draw(Graphics arg0, Rectangle arg1, Rectangle arg2,
			double arg3, boolean arg4) {	
		 
		 super.draw(arg0, arg1, arg2, arg3, arg4);
	}


	// snipped rest of calls to super functions

        .
        .
        .
	

	public static void main(String[] args) {
		
	}


}

Finally the code for MyBoard.java:

package MySnapTo;

import java.awt.Point;

import MySnapTo.MyHexGrid;
import VASSAL.build.module.map.boardPicker.Board;
import VASSAL.build.module.map.boardPicker.board.HexGrid;

public class MyBoard extends Board {
	

	public MyBoard() {
		
		super();
		
	}
	
	
	public Point snapTo(Point p, int facing) {
		
		if (grid instanceof HexGrid)
		{
			MyHexGrid mgrid = (MyHexGrid)grid;
			return mgrid == null ? p : globalCoordinates(mgrid.snapTo(localCoordinates(p), facing));
		}
		else
			return grid == null ? p : globalCoordinates(grid.snapTo(localCoordinates(p)));
		
	  }

}

I changed the buildFile to use the new classes MyMap, MyBoard and MyHexGrid (from the package MySnapTo):

<MySnapTo.MyMap allowMultiple="false" backgroundcolor="255,255,255"  ...... >
        <VASSAL.build.module.map.BoardPicker addColumnText="Add column" ....... >
            <MySnapTo.MyBoard image="wsim-new.png" name="Board 1" reversible="false">
                <MySnapTo.MyHexGrid color="102,102,102" cornersLegal="false" ....... >
                    <VASSAL.build.module.map.boardPicker.board.mapgrid.HexGridNumbering ...... >
                </MySnapTo.MyHexGrid>
            </MySnapTo.MyBoard>
        </VASSAL.build.module.map.BoardPicker>
   
	.
	.

</MySnapTo.MyMap>

It all looks simple when done, but understanding and modifying software you have not made yourself is never easy.

A few issues:

  1. When a counter is first dragged onto the map it sometimes snaps wrongly. I have not been able to reproduce this error consistently.
  2. I discovered that the GamePiece array increases in length by simply pivoting a counter (?)
  3. Also note that the code above works only for a map with non-stackable counters. If stacking is allowed then a GamePiece must be cast to a Stack and topPiece() called.
  4. How do you load a custom class that is not Buildable (for instance an extension of a class in Counters)?

But it was fun doing this. I learned a lot and I have to compliment the developers that made VASSAL.

Hello Rhett,

do you have any plans to release your new WS&IM module ?

Best Regards, Matthias.

I don’t think releasing the module is a good idea, as there still are some issues (bugs).
But I can send you the module so you can test it, and perhaps I (or you) can fix what
doesn’t work.

I can send you the module if you give me your mail address (as a private message).

Rhett

Thank you, and PM sent.
Matthias